mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-08-29 18:31:33 +00:00
fix OIDC verification for JWKs without alg (#1102)
This commit is contained in:
@@ -215,9 +215,8 @@ export async function verifyOIDCToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const header = JSON.parse(
|
const { decodeProtectedHeader, importJWK, jwtVerify } = await import("jose");
|
||||||
Buffer.from(idToken.split(".")[0], "base64").toString(),
|
const header = decodeProtectedHeader(idToken);
|
||||||
);
|
|
||||||
const keyId = header.kid;
|
const keyId = header.kid;
|
||||||
|
|
||||||
const publicKey = jwks.keys.find(
|
const publicKey = jwks.keys.find(
|
||||||
@@ -229,8 +228,9 @@ export async function verifyOIDCToken(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { importJWK, jwtVerify } = await import("jose");
|
const algorithm =
|
||||||
const key = await importJWK(publicKey);
|
typeof publicKey.alg === "string" ? publicKey.alg : header.alg;
|
||||||
|
const key = await importJWK(publicKey, algorithm);
|
||||||
|
|
||||||
const { payload } = await jwtVerify(idToken, key, {
|
const { payload } = await jwtVerify(idToken, key, {
|
||||||
issuer: possibleIssuers,
|
issuer: possibleIssuers,
|
||||||
|
|||||||
@@ -16,11 +16,50 @@ const {
|
|||||||
getOIDCConfigFromEnv,
|
getOIDCConfigFromEnv,
|
||||||
extractOidcGroups,
|
extractOidcGroups,
|
||||||
validateLogoutTokenClaims,
|
validateLogoutTokenClaims,
|
||||||
|
verifyOIDCToken,
|
||||||
} = await import("../../../database/routes/user-oidc-utils.js");
|
} = await import("../../../database/routes/user-oidc-utils.js");
|
||||||
|
|
||||||
const BACKCHANNEL_LOGOUT_EVENT =
|
const BACKCHANNEL_LOGOUT_EVENT =
|
||||||
"http://schemas.openid.net/event/backchannel-logout";
|
"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", () => {
|
describe("isOIDCUserAllowed", () => {
|
||||||
it("allows everyone when the allow-list is empty", () => {
|
it("allows everyone when the allow-list is empty", () => {
|
||||||
expect(isOIDCUserAllowed("", "alice", "alice@x.com")).toBe(true);
|
expect(isOIDCUserAllowed("", "alice", "alice@x.com")).toBe(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user