From b7db37828b9b0018751b0dc7cf48b5ef2d5ec2f9 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 30 Mar 2026 11:16:44 +0100 Subject: [PATCH] Fix 401: send ID token instead of Graph access token Access tokens for User.Read scope have audience=graph.microsoft.com, but the backend validates audience=CLIENT_ID. ID tokens always have audience=CLIENT_ID so they validate correctly. Also add upn claim fallback for email extraction from ID token. Co-Authored-By: Claude Sonnet 4.6 --- backend/app/middleware/auth.py | 6 +++++- frontend/src/api/client.ts | 5 +++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/backend/app/middleware/auth.py b/backend/app/middleware/auth.py index 583acb6..1eb29e0 100644 --- a/backend/app/middleware/auth.py +++ b/backend/app/middleware/auth.py @@ -68,7 +68,11 @@ async def get_current_user( return { "oid": payload.get("oid"), "name": payload.get("name"), - "email": payload.get("preferred_username") or payload.get("email"), + "email": ( + payload.get("preferred_username") + or payload.get("upn") + or payload.get("email") + ), } except JWTError as e: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=f"Invalid token: {e}") diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 90f8eda..5e8f02f 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -13,10 +13,11 @@ api.interceptors.request.use(async (config) => { try { const result = await msalInstance.acquireTokenSilent({ - ...loginRequest, + scopes: ['openid', 'profile', 'email'], account: accounts[0], }); - config.headers.Authorization = `Bearer ${result.accessToken}`; + // ID token has audience=CLIENT_ID so the backend can validate it + config.headers.Authorization = `Bearer ${result.idToken}`; } catch { // Token expired or failed — trigger interactive login await msalInstance.loginRedirect(loginRequest);