ActiveSync: credential-less EAS request returns 403 instead of a 401 challenge, silently breaking push

Author: luckykietCreated Sep 8, 2026Updated Sep 8, 2026

Summary

data/web/sogo-auth.php returns HTTP 200 with no X-Auth header when an internal auth_request subrequest arrives without credentials. nginx then overwrites the client's Authorization header with an empty value, SOGo receives an anonymous request and answers 403.

Per RFC 9110 a protected resource reached without credentials must answer 401 + WWW-Authenticate. Clients that rely on challenge-response rather than pre-emptive Basic auth therefore cannot provision an ActiveSync device at all: they read 403 as fatal and give up. Outlook for iOS silently falls back to IMAP, which on iOS cannot deliver push notifications, so the user-visible symptom is "no push" with nothing obviously wrong.

Affected

  • mailcow-dockerized (observed 2026-09), SOGo ActiveSync, SKIP_SOGO=n
  • Any EAS client that does not send pre-emptive Basic auth on every request. Confirmed with Outlook for iOS (which proxies EAS via Microsoft's cloud, source IPs in 40.104.0.0/16, 52.98.0.0/16).

Root cause

data/web/sogo-auth.php:

php
$is_internal_auth = (($_SERVER['SOGO_AUTH_INTERNAL'] ?? '') === '1');

if ($is_internal_auth && isset($_SERVER['PHP_AUTH_USER'])) {
    ...
    if ($login_check === 'user') {
        header("X-User: $username");
        header("X-Auth: Basic ".base64_encode("$username:$password"));
        exit;
    } else {
        header('HTTP/1.0 401 Unauthorized');   // bad credentials -> correct 401
        exit;
    }
}
elseif (isset($_GET['login'])) { ... }
// no credentials at all -> falls through both branches -> implicit 200, no headers

data/conf/nginx/templates/sites-default.conf.j2:

nginx
location ^~ /Microsoft-Server-ActiveSync {
    auth_request /sogo-auth-verify;
    auth_request_set $auth $upstream_http_x_auth;
    proxy_set_header Authorization "$auth";   # empty when X-Auth absent
    proxy_pass http://{{ SOGOHOST }}:20000/SOGo/Microsoft-Server-ActiveSync;
}

With $auth empty, SOGo logs and returns:

<SOGoActiveSyncDispatcher> EAS - Forbidden access for user (null)
POST /SOGo/Microsoft-Server-ActiveSync?Cmd=Provision... 403

Note the asymmetry: wrong credentials correctly yield 401, but absent credentials yield 403.

Reproduction

bash
# no credentials -> 403 (should be 401 + WWW-Authenticate)
curl -si -X POST 'https://<host>/Microsoft-Server-ActiveSync?Cmd=Provision&[email protected]&DeviceId=x&DeviceType=iPhone'

# wrong credentials -> 401 (correct)
curl -si -X POST -u '[email protected]:wrong' 'https://<host>/Microsoft-Server-ActiveSync?Cmd=Provision&...'

# OPTIONS without credentials -> 401 (correct; SOGo challenges here)
curl -si -X OPTIONS 'https://<host>/Microsoft-Server-ActiveSync'

Observed client sequence before the fix (Outlook iOS):

OPTIONS Cmd=Options  -> 401   challenge
OPTIONS Cmd=Options  -> 200   authenticated
POST    Cmd=Provision -> 403  no pre-emptive auth; expected a challenge, got a hard failure

php-fpm access log for the same three requests shows the auth user as blank -> user@domain -> blank, confirming the POST carried no Authorization header.

Proposed fix

Issue a challenge instead of falling through, scoped to the ActiveSync path so SOGo web (?login=) and DAV are unaffected:

php
elseif ($is_internal_auth &&
  preg_match('/^(\/SOGo|)\/Microsoft-Server-ActiveSync.*/',
    $_SERVER['HTTP_X_ORIGINAL_URI'] ?? '') === 1) {
  header('WWW-Authenticate: Basic realm="SOGo"');
  header('HTTP/1.0 401 Unauthorized');
  exit;
}

nginx's auth_request propagates both the 401 and the WWW-Authenticate header to the client. A broader fix (challenging for any internal auth subrequest without credentials) may be preferable, but was not tested here.

Verification after patching

Cmd=Provision      -> 200
Cmd=FolderSync     -> 200
Cmd=Sync           -> 200
Cmd=Ping  held 180.1s     (idle heartbeat)
Cmd=Ping  held  45.1s     (returned early on new mail)
"Change detected during Sync, we push the content."

doveadm who then shows the persistent IMAP session SOGo holds on the user's behalf, which is absent while EAS is broken.

Impact

Silent. EAS never provisions, the client degrades to IMAP without surfacing an error, and push simply never arrives. Nothing in the mailcow UI indicates a problem, and eas_access is enabled on the mailbox throughout.

Source: mailcow/mailcow-dockerized