fix OIDC verification for JWKs without alg (#1102)

This commit is contained in:
ZacharyZcR
2026-07-28 01:48:03 +08:00
committed by GitHub
parent cf827f9a9c
commit 6866176778
2 changed files with 44 additions and 5 deletions
@@ -215,9 +215,8 @@ export async function verifyOIDCToken(
);
}
const header = JSON.parse(
Buffer.from(idToken.split(".")[0], "base64").toString(),
);
const { decodeProtectedHeader, importJWK, jwtVerify } = await import("jose");
const header = decodeProtectedHeader(idToken);
const keyId = header.kid;
const publicKey = jwks.keys.find(
@@ -229,8 +228,9 @@ export async function verifyOIDCToken(
);
}
const { importJWK, jwtVerify } = await import("jose");
const key = await importJWK(publicKey);
const algorithm =
typeof publicKey.alg === "string" ? publicKey.alg : header.alg;
const key = await importJWK(publicKey, algorithm);
const { payload } = await jwtVerify(idToken, key, {
issuer: possibleIssuers,
@@ -16,11 +16,50 @@ const {
getOIDCConfigFromEnv,
extractOidcGroups,
validateLogoutTokenClaims,
verifyOIDCToken,
} = await import("../../../database/routes/user-oidc-utils.js");
const BACKCHANNEL_LOGOUT_EVENT =
"http://schemas.openid.net/event/backchannel-logout";
afterEach(() => {
vi.restoreAllMocks();
});
describe("verifyOIDCToken", () => {
it("uses the protected-header algorithm when the provider JWK omits alg", async () => {
const { exportJWK, generateKeyPair, SignJWT } = await import("jose");
const { publicKey, privateKey } = await generateKeyPair("RS256");
const jwk = await exportJWK(publicKey);
jwk.kid = "entra-key";
const issuer = "https://login.microsoftonline.com/example/v2.0";
const clientId = "termix-client";
const token = await new SignJWT({ sub: "user-1" })
.setProtectedHeader({ alg: "RS256", kid: jwk.kid })
.setIssuer(issuer)
.setAudience(clientId)
.setExpirationTime("5m")
.sign(privateKey);
const fetchMock = vi
.spyOn(globalThis, "fetch")
.mockResolvedValueOnce(
new Response(JSON.stringify({ jwks_uri: "https://idp.example/keys" }), {
status: 200,
}),
)
.mockResolvedValueOnce(
new Response(JSON.stringify({ keys: [jwk] }), { status: 200 }),
);
const payload = await verifyOIDCToken(token, issuer, clientId);
expect(payload.sub).toBe("user-1");
expect(fetchMock).toHaveBeenCalledTimes(2);
});
});
describe("isOIDCUserAllowed", () => {
it("allows everyone when the allow-list is empty", () => {
expect(isOIDCUserAllowed("", "alice", "alice@x.com")).toBe(true);