ActiveSync: credential-less EAS request returns 403 instead of a 401 challenge, silently breaking push
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:
$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 headersdata/conf/nginx/templates/sites-default.conf.j2:
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... 403Note the asymmetry: wrong credentials correctly yield 401, but absent credentials yield 403.
Reproduction
# 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 failurephp-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:
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