From 0e6f5be46db75c24cf6c065133307230483a37c0 Mon Sep 17 00:00:00 2001 From: michael Date: Thu, 18 Dec 2025 17:03:47 -0600 Subject: [PATCH] Support both v1.0 and v2.0 Azure AD token issuer formats MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Azure AD issues tokens with different issuer formats depending on the app registration's accessTokenAcceptedVersion setting: - v1.0: https://sts.windows.net/{tenant}/ - v2.0: https://login.microsoftonline.com/{tenant}/v2.0 Update backend to accept both formats by trying each issuer in sequence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- backend/app/services/auth_service.py | 40 +++++++++++++++++++--------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py index 8745c6f..4b4b126 100755 --- a/backend/app/services/auth_service.py +++ b/backend/app/services/auth_service.py @@ -120,19 +120,35 @@ async def verify_access_token(token: str) -> Optional[dict]: logger.info(f"[MSAL Backend] Found matching RSA key for kid: {kid}") # Verify and decode the token - # For ID tokens with OpenID scopes, audience is the client ID - # and issuer uses the v2.0 endpoint - expected_issuer = f"https://login.microsoftonline.com/{settings.AZURE_TENANT_ID}/v2.0" - logger.info(f"[MSAL Backend] Verifying with audience: {settings.AZURE_CLIENT_ID}") - logger.info(f"[MSAL Backend] Verifying with issuer: {expected_issuer}") + # Azure AD can issue tokens with either v1.0 or v2.0 issuer format + # depending on the app registration's accessTokenAcceptedVersion setting + v1_issuer = f"https://sts.windows.net/{settings.AZURE_TENANT_ID}/" + v2_issuer = f"https://login.microsoftonline.com/{settings.AZURE_TENANT_ID}/v2.0" - claims = jwt.decode( - token, - rsa_key, - algorithms=["RS256"], - audience=settings.AZURE_CLIENT_ID, - issuer=expected_issuer, - ) + logger.info(f"[MSAL Backend] Verifying with audience: {settings.AZURE_CLIENT_ID}") + logger.info(f"[MSAL Backend] Accepting issuers: {v1_issuer} OR {v2_issuer}") + + # Try v1 issuer first (most common for /.default scope tokens) + claims = None + for issuer in [v1_issuer, v2_issuer]: + try: + claims = jwt.decode( + token, + rsa_key, + algorithms=["RS256"], + audience=settings.AZURE_CLIENT_ID, + issuer=issuer, + ) + logger.info(f"[MSAL Backend] Token verified with issuer: {issuer}") + break + except JWTError as e: + if "issuer" in str(e).lower(): + continue # Try next issuer + raise # Re-raise if it's a different error + + if not claims: + logger.warning("[MSAL Backend] Token issuer doesn't match any expected format") + return None logger.info(f"[MSAL Backend] Token verified successfully!") logger.info(f"[MSAL Backend] User: {claims.get('name', 'unknown')} ({claims.get('preferred_username', 'unknown')})")