Skip to content

Commit 82a1d17

Browse files
committed
test(auth): cover Microsoft id-token emailVerified derivation
Extract the Microsoft ID-token email-verification logic into a pure deriveMicrosoftEmailVerified helper and add unit coverage for explicit, verified-claim, partial, absent, and malformed Azure AD claim combinations.
1 parent d6c816d commit 82a1d17

3 files changed

Lines changed: 101 additions & 13 deletions

File tree

apps/sim/lib/auth/auth.ts

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,11 @@ import { isSignInProviderAllowed } from './constants'
9292

9393
const logger = createLogger('Auth')
9494

95-
import { getMicrosoftRefreshTokenExpiry, isMicrosoftProvider } from '@/lib/oauth/microsoft'
95+
import {
96+
deriveMicrosoftEmailVerified,
97+
getMicrosoftRefreshTokenExpiry,
98+
isMicrosoftProvider,
99+
} from '@/lib/oauth/microsoft'
96100
import { getCanonicalScopesForProvider } from '@/lib/oauth/utils'
97101

98102
/**
@@ -129,18 +133,7 @@ function getMicrosoftUserInfoFromIdToken(tokens: { accessToken?: string }, provi
129133
)
130134
}
131135

132-
/**
133-
* Azure AD's `email`/`upn` claims are unverified and mutable on multi-tenant
134-
* (`/common/`) endpoints, so trust the email only when the token explicitly
135-
* proves ownership via `email_verified` or the verified-email claims, mirroring
136-
* Better Auth's built-in Microsoft provider. Never hardcode verification.
137-
*/
138-
const verifiedPrimaryEmail = payload.verified_primary_email as string[] | undefined
139-
const verifiedSecondaryEmail = payload.verified_secondary_email as string[] | undefined
140-
const emailVerified =
141-
payload.email_verified !== undefined
142-
? Boolean(payload.email_verified)
143-
: Boolean(verifiedPrimaryEmail?.includes(email) || verifiedSecondaryEmail?.includes(email))
136+
const emailVerified = deriveMicrosoftEmailVerified(payload, email)
144137

145138
const now = new Date()
146139
return {
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it } from 'vitest'
5+
import { deriveMicrosoftEmailVerified, isMicrosoftProvider } from '@/lib/oauth/microsoft'
6+
7+
const EMAIL = 'user@contoso.com'
8+
9+
describe('deriveMicrosoftEmailVerified', () => {
10+
it('honors an explicit email_verified=true claim', () => {
11+
expect(deriveMicrosoftEmailVerified({ email_verified: true }, EMAIL)).toBe(true)
12+
})
13+
14+
it('honors an explicit email_verified=false claim over verified-email claims', () => {
15+
expect(
16+
deriveMicrosoftEmailVerified(
17+
{ email_verified: false, verified_primary_email: [EMAIL] },
18+
EMAIL
19+
)
20+
).toBe(false)
21+
})
22+
23+
it('treats a verified primary email matching the email as verified', () => {
24+
expect(deriveMicrosoftEmailVerified({ verified_primary_email: [EMAIL] }, EMAIL)).toBe(true)
25+
})
26+
27+
it('treats a verified secondary email matching the email as verified', () => {
28+
expect(
29+
deriveMicrosoftEmailVerified({ verified_secondary_email: ['x@y.com', EMAIL] }, EMAIL)
30+
).toBe(true)
31+
})
32+
33+
it('does not verify when the verified-email claims do not include the email', () => {
34+
expect(
35+
deriveMicrosoftEmailVerified(
36+
{
37+
verified_primary_email: ['other@contoso.com'],
38+
verified_secondary_email: ['another@contoso.com'],
39+
},
40+
EMAIL
41+
)
42+
).toBe(false)
43+
})
44+
45+
it('defaults to false when no verification claim is present (typical Azure AD token)', () => {
46+
expect(deriveMicrosoftEmailVerified({ name: 'User', oid: 'abc' }, EMAIL)).toBe(false)
47+
})
48+
49+
it('defaults to false for an empty claim set', () => {
50+
expect(deriveMicrosoftEmailVerified({}, EMAIL)).toBe(false)
51+
})
52+
53+
it('coerces a truthy non-boolean email_verified claim', () => {
54+
expect(deriveMicrosoftEmailVerified({ email_verified: 'true' }, EMAIL)).toBe(true)
55+
})
56+
57+
it('treats a malformed verified-email claim as unverified', () => {
58+
expect(deriveMicrosoftEmailVerified({ verified_primary_email: 'not-an-array' }, EMAIL)).toBe(
59+
false
60+
)
61+
})
62+
})
63+
64+
describe('isMicrosoftProvider', () => {
65+
it('recognizes Microsoft connector provider IDs', () => {
66+
expect(isMicrosoftProvider('microsoft-ad')).toBe(true)
67+
expect(isMicrosoftProvider('outlook')).toBe(true)
68+
})
69+
70+
it('rejects non-Microsoft provider IDs', () => {
71+
expect(isMicrosoftProvider('google')).toBe(false)
72+
expect(isMicrosoftProvider('microsoft')).toBe(false)
73+
})
74+
})

apps/sim/lib/oauth/microsoft.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,24 @@ export function isMicrosoftProvider(providerId: string): boolean {
1919
export function getMicrosoftRefreshTokenExpiry(): Date {
2020
return new Date(Date.now() + MICROSOFT_REFRESH_TOKEN_LIFETIME_DAYS * 24 * 60 * 60 * 1000)
2121
}
22+
23+
/**
24+
* Derives whether a Microsoft ID token proves ownership of `email`. Azure AD's
25+
* `email`/`upn` claims are unverified and mutable on multi-tenant (`/common/`)
26+
* endpoints, so the email is trusted only when the token explicitly proves it via
27+
* the `email_verified` claim or the verified-email claims, mirroring Better
28+
* Auth's built-in Microsoft provider. Defaults to `false` when no claim asserts
29+
* verification, so an attacker-controlled tenant can never assert a verified
30+
* email it does not own.
31+
*/
32+
export function deriveMicrosoftEmailVerified(
33+
claims: Record<string, unknown>,
34+
email: string
35+
): boolean {
36+
if (claims.email_verified !== undefined) {
37+
return Boolean(claims.email_verified)
38+
}
39+
const verifiedPrimaryEmail = claims.verified_primary_email as string[] | undefined
40+
const verifiedSecondaryEmail = claims.verified_secondary_email as string[] | undefined
41+
return Boolean(verifiedPrimaryEmail?.includes(email) || verifiedSecondaryEmail?.includes(email))
42+
}

0 commit comments

Comments
 (0)