Add EntraAppClient for Microsoft Entra ID (Azure AD) oauth2 - #920
Open
urskog84 wants to merge 1 commit into
Open
Add EntraAppClient for Microsoft Entra ID (Azure AD) oauth2#920urskog84 wants to merge 1 commit into
urskog84 wants to merge 1 commit into
Conversation
Closes AnswerDotAI#718. Entra needs two things beyond the usual client boilerplate. It normalises the scopes it grants, so they don't always match what was requested: asking for `https://graph.microsoft.com/User.Read`, the form Microsoft's samples use, comes back as plain `User.Read`, and oauthlib raises `Warning: Scope has changed` on the difference. `parse_request_body_response` skips that check, then restores the requested scope. And directory-level identity lives in the `id_token` rather than in Graph's userinfo, so `get_info` merges the two, making `oid` (immutable user id) and `tid` (tenant) available to `get_auth`. `tenant_id` is required rather than defaulting to 'common', so that the single-tenant case - where Entra itself turns away other directories - is what you get unless you ask for more. Verified against a live single-tenant app registration: login, token exchange, Graph userinfo and logout. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
urskog84
force-pushed
the
entra-oauth-client
branch
from
August 16, 2026 15:17
8bea150 to
f1cf9c8
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #718.
Adds
EntraAppClient(exported asAzureAppClienttoo) for Microsoft Entra ID, formerly Azure AD.Why it isn't just another endpoint triple
Two things make Entra different from the existing clients:
Entra normalises the scopes it grants, so they don't always match what was requested, and oauthlib's
validate_token_parametersraisesWarning: Scope has changedwhen the sets differ. Requestinghttps://graph.microsoft.com/User.Read— the form Microsoft's own samples and the original issue use — comes back as plainUser.Read, which is enough to break the token exchange.parse_request_body_responsetherefore skips the scope-change check, and restores the requested scope afterwards so later login links are unaffected.Directory identity isn't in Graph's userinfo.
/oidc/userinfogivessub,name,email; the claims apps actually need to authorise against a directory —oid(the user's immutable ID) andtid(the tenant they signed in from) — are only in theid_token.get_info/get_info_asyncmerge the two, soget_authreceives both. Decoding is a stdlib base64 unpack of the JWT payload rather than a PyJWT dependency, since the token arrives straight from the token endpoint over TLS and no signature check is performed either way.API choices worth a second opinion
tenant_idis required, not defaulted to'common'. A concrete directory ID means Entra rejects other directories before the request ever reaches the app, so the safe case is the default one;'organizations'and'common'are still there for anyone who wants them, and the docs spell out that they then owe their app atidcheck.identstayssub(unique per user per app, consistent with the other clients). Apps that want the directory-wide ID can setclient.id_key = 'oid'.logout_link()is included because/logoutalone leaves the user signed in to Microsoft, so the next login round-trips silently — surprising in an enterprise SSO setting.openid profile email User.Read, in short form. The OIDC scopes alone are enough for/oidc/userinfo;User.Readis user-consentable and makes the Graph audience unambiguous.Testing
Run end to end against a real single-tenant Entra app registration (login, token exchange, Graph userinfo, logout), plus offline checks against mocked responses.
Worth recording what the live runs showed about point 1, since it decides whether that code earns its place:
openid profile email User.Reademail openid profile User.Readopenid profile email https://graph.microsoft.com/User.Reademail openid profile User.ReadWarning: Scope has changedSo the client's own default never trips the check, but anyone following Microsoft's docs and passing Graph scopes as URIs does, and gets a login that fails at the token exchange. Same applies when a tenant grants scopes beyond those requested.
The merged
infocarriedsub,oid,tid,preferred_username,email,nameandpicture, withtidmatching the tenant. Note it also carries the id_token's own metadata claims (aud,iss,exp,nbf,iat,sid,uti,ver) — happy to filter those out if you'd ratherinfostayed lean.Offline checks (authorize URL, pass-through params like
prompt, token exchange payload, scope restoration, sync and asyncretr_info,id_keyoverride, bothlogout_linkforms) live outside this PR — say the word and I'll add them astests/test_oauth.py.nbs/api/08_oauth.ipynbis the edited source;fasthtml/oauth.pyand_modidx.pycame fromnbdev_export.nbs/apilist.txthas only the new section added by hand — regenerating the whole file pulled in unrelated drift (htmx4,vurl, …) that seemed to belong in its own commit.🤖 Generated with Claude Code