mirror of
https://github.com/better-auth/better-auth.git
synced 2026-10-20 20:26:55 -05:00
fix(core): verify microsoft entra id tokens when jwks keys omit alg (#11663)
Co-authored-by: Gustavo Valverde <g.valverde02@gmail.com>
This commit is contained in:
co-authored by
Gustavo Valverde
parent
e9d9921217
commit
60525e08fd
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@better-auth/core": patch
|
||||
---
|
||||
|
||||
Fixed Microsoft Entra ID sign-in with an ID token (`signIn.social` with `idToken`, and linking an account with an ID token) rejecting every token. Microsoft publishes its signing keys without an `alg` field, which made verification fail. Those keys are now treated as RS256, and Microsoft ID tokens signed with any other algorithm are rejected. The redirect sign-in flow was not affected.
|
||||
@@ -14,6 +14,7 @@ import { microsoft } from "./microsoft-entra-id";
|
||||
const mockedBetterFetch = vi.mocked(betterFetch);
|
||||
|
||||
const CLIENT_ID = "ms-app";
|
||||
const CLIENT_SECRET = "ms-secret";
|
||||
const AUTHORITY = "https://login.microsoftonline.com";
|
||||
const CONSUMER_TENANT_ID = "9188040d-6c67-4c5b-b112-36a304b66dad";
|
||||
const WORK_TENANT_ID = "11111111-2222-3333-4444-555555555555";
|
||||
@@ -27,7 +28,6 @@ beforeEach(async () => {
|
||||
privateKey = keyPair.privateKey;
|
||||
publicJWK = await exportJWK(keyPair.publicKey);
|
||||
publicJWK.kid = KID;
|
||||
publicJWK.alg = "RS256";
|
||||
publicJWK.use = "sig";
|
||||
|
||||
mockedBetterFetch.mockReset();
|
||||
@@ -278,3 +278,64 @@ describe("microsoft account subject", () => {
|
||||
expect(mockedBetterFetch).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* @see https://github.com/better-auth/better-auth/issues/11652
|
||||
*/
|
||||
describe("microsoft id_token alg omission", () => {
|
||||
it("verifies a token when Microsoft's JWKS keys omit alg", async () => {
|
||||
const provider = microsoft({
|
||||
clientId: CLIENT_ID,
|
||||
clientSecret: CLIENT_SECRET,
|
||||
});
|
||||
|
||||
const token = await new SignJWT({
|
||||
sub: "user-123",
|
||||
email: "user@example.com",
|
||||
tid: WORK_TENANT_ID,
|
||||
})
|
||||
.setProtectedHeader({ alg: "RS256", kid: KID })
|
||||
.setIssuer(`${AUTHORITY}/${WORK_TENANT_ID}/v2.0`)
|
||||
.setAudience(CLIENT_ID)
|
||||
.setExpirationTime("1h")
|
||||
.setIssuedAt()
|
||||
.sign(privateKey);
|
||||
|
||||
const isValid = await verifyProviderIdToken(provider, token);
|
||||
expect(isValid).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects a token signed with an algorithm other than RS256", async () => {
|
||||
const ecKeyPair = await generateKeyPair("ES256", { extractable: true });
|
||||
const ecJWK = await exportJWK(ecKeyPair.publicKey);
|
||||
const ecKid = "ms-test-es256-key";
|
||||
ecJWK.kid = ecKid;
|
||||
ecJWK.alg = "ES256";
|
||||
ecJWK.use = "sig";
|
||||
|
||||
mockedBetterFetch.mockResolvedValueOnce({
|
||||
data: { keys: [ecJWK] },
|
||||
error: null,
|
||||
} as Awaited<ReturnType<typeof betterFetch>>);
|
||||
|
||||
const provider = microsoft({
|
||||
clientId: CLIENT_ID,
|
||||
clientSecret: CLIENT_SECRET,
|
||||
});
|
||||
|
||||
const token = await new SignJWT({
|
||||
sub: "user-123",
|
||||
email: "user@example.com",
|
||||
tid: WORK_TENANT_ID,
|
||||
})
|
||||
.setProtectedHeader({ alg: "ES256", kid: ecKid })
|
||||
.setIssuer(`${AUTHORITY}/${WORK_TENANT_ID}/v2.0`)
|
||||
.setAudience(CLIENT_ID)
|
||||
.setExpirationTime("1h")
|
||||
.setIssuedAt()
|
||||
.sign(ecKeyPair.privateKey);
|
||||
|
||||
const isValid = await verifyProviderIdToken(provider, token);
|
||||
expect(isValid).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -228,6 +228,7 @@ export const microsoft = (options: MicrosoftOptions) => {
|
||||
},
|
||||
idToken: {
|
||||
jwks: (header) => getMicrosoftPublicKey(header.kid!, tenant, authority),
|
||||
algorithms: ["RS256"],
|
||||
audience: options.clientId,
|
||||
maxTokenAge: "1h",
|
||||
/**
|
||||
@@ -370,7 +371,7 @@ export const getMicrosoftPublicKey = async (
|
||||
const { data } = await betterFetch<{
|
||||
keys: Array<{
|
||||
kid: string;
|
||||
alg: string;
|
||||
alg?: string;
|
||||
kty: string;
|
||||
use: string;
|
||||
n: string;
|
||||
@@ -391,5 +392,6 @@ export const getMicrosoftPublicKey = async (
|
||||
throw new Error(`JWK with kid ${kid} not found`);
|
||||
}
|
||||
|
||||
return await importJWK(jwk, jwk.alg);
|
||||
// Microsoft's key set omits `alg`; Entra ID signs ID tokens with RS256.
|
||||
return await importJWK(jwk, jwk.alg ?? "RS256");
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user