* Extracts user info from a Microsoft ID token JWT instead of calling Graph API /me. * This avoids 403 errors for external tenant users whose admin hasn't consented to Graph API scopes. * The ID token is always returned when the openid scope is requested.
(tokens: { accessToken?: string }, providerId: string)
| 105 | * The ID token is always returned when the openid scope is requested. |
| 106 | */ |
| 107 | function getMicrosoftUserInfoFromIdToken(tokens: { accessToken?: string }, providerId: string) { |
| 108 | const idToken = (tokens as Record<string, unknown>).idToken as string | undefined |
| 109 | if (!idToken) { |
| 110 | logger.error( |
| 111 | `Microsoft ${providerId} OAuth: no ID token received. Ensure openid scope is requested.` |
| 112 | ) |
| 113 | throw new Error(`Microsoft ${providerId} OAuth requires an ID token (openid scope)`) |
| 114 | } |
| 115 | |
| 116 | const parts = idToken.split('.') |
| 117 | if (parts.length !== 3) { |
| 118 | throw new Error(`Microsoft ${providerId} OAuth: malformed ID token`) |
| 119 | } |
| 120 | |
| 121 | let payload: Record<string, unknown> |
| 122 | try { |
| 123 | payload = JSON.parse(Buffer.from(parts[1], 'base64url').toString('utf-8')) |
| 124 | } catch { |
| 125 | throw new Error(`Microsoft ${providerId} OAuth: failed to decode ID token payload`) |
| 126 | } |
| 127 | |
| 128 | const email = |
| 129 | (payload.email as string) || (payload.preferred_username as string) || (payload.upn as string) |
| 130 | if (!email) { |
| 131 | throw new Error( |
| 132 | `Microsoft ${providerId} OAuth: ID token contains no email, preferred_username, or upn claim` |
| 133 | ) |
| 134 | } |
| 135 | |
| 136 | const emailVerified = deriveMicrosoftEmailVerified(payload, email) |
| 137 | |
| 138 | const now = new Date() |
| 139 | return { |
| 140 | id: `${payload.oid || payload.sub}-${generateId()}`, |
| 141 | name: (payload.name as string) || 'Microsoft User', |
| 142 | email, |
| 143 | emailVerified, |
| 144 | createdAt: now, |
| 145 | updatedAt: now, |
| 146 | } |
| 147 | } |
| 148 | |
| 149 | const additionalTrustedOrigins = parseOriginList(env.TRUSTED_ORIGINS, (value) => |
| 150 | logger.warn('Ignoring invalid entry in TRUSTED_ORIGINS', { value }) |
no test coverage detected