fix(microsoft-shared): drop User.Read from imap_* scopes to keep token audience on Outlook

Production diagnostic logged:
  scopes='email IMAP.AccessAsUser.All Mail.ReadWrite.Shared Mail.Send.Shared
          openid profile User.Read'
  aud='00000003-0000-0000-c000-000000000000'

The aud is Microsoft Graph's well-known app ID. Outlook's IMAP server only
accepts tokens with aud=https://outlook.office.com — Graph-aud tokens get
rejected with NoResponseError 'AUTHENTICATE failed.' even when the granted
scope list looks right.

Cause: a single Microsoft v2.0 OAuth response can only have one audience.
Mixing Graph-resource scopes (User.Read) with Outlook-resource scopes
(https://outlook.office.com/...) in one request makes Microsoft pick a
single audience for the token. With User.Read present, it picks Graph.

Fix: imap_smtp and imap_leadmail now request only:
- OIDC scopes (offline_access, openid, profile, email) — universal, don't
  pin audience and still produce an id_token for oauth_user_email
- Outlook-resource scopes (https://outlook.office.com/IMAP.AccessAsUser.All
  and SMTP.Send for imap_smtp)

graph_full keeps its Graph scope set unchanged.

User-side caveat: previously-consented Mail.*.Shared scopes from earlier
graph_full testing are still in Niklas's consent cache and Microsoft will
reflect them back in scp on any new OAuth, potentially re-tainting the
audience choice. Niklas needs to revoke the LeadChat app from
https://myaccount.microsoft.com -> Privacy -> Apps and services to fully
clear those before retrying.
This commit is contained in:
netlas
2026-04-28 11:12:47 +03:00
parent 78a5d01a8c
commit 235b990ef9

View File

@@ -16,12 +16,20 @@ module MicrosoftSharedConcern
Mail.Send.Shared
Mail.ReadWrite.Shared
].freeze,
# imap_* transports MUST request only Outlook-resource scopes (URL-prefixed
# https://outlook.office.com/...) plus universal OIDC scopes. Adding any
# Graph-resource scope (User.Read, Mail.*, etc.) causes Microsoft to issue
# a token with audience=Microsoft Graph (00000003-0000-0000-c000-000000000000)
# which the Outlook IMAP/SMTP servers reject with 'AUTHENTICATE failed'.
# OIDC scopes (openid, profile, email, offline_access) are universal and
# don't pin the audience — they ride alongside whatever resource scope is
# requested. The id_token they produce gives us oauth_user_email without
# needing User.Read.
'imap_smtp' => %w[
offline_access
openid
profile
email
User.Read
https://outlook.office.com/IMAP.AccessAsUser.All
https://outlook.office.com/SMTP.Send
].freeze,
@@ -30,7 +38,6 @@ module MicrosoftSharedConcern
openid
profile
email
User.Read
https://outlook.office.com/IMAP.AccessAsUser.All
].freeze
}.freeze