Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion packages/backend/src/services/oidc.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,10 @@ export async function validateIdToken(idToken: string, nonce: string): Promise<O
jwksCache = { uri: doc.jwks_uri, set: createRemoteJWKSet(new URL(doc.jwks_uri)) };
}
const { payload } = await jwtVerify(idToken, jwksCache.set, {
issuer: env.OIDC_ISSUER_URL,
// getEnv normalizes the configured URL, but providers such as Authentik
// publish a scoped issuer with a trailing slash in the discovery document
// and id_token. Accept both equivalent wire representations.
issuer: [env.OIDC_ISSUER_URL, `${env.OIDC_ISSUER_URL}/`],
audience: env.OIDC_CLIENT_ID,
});
if (payload.nonce !== nonce) {
Expand Down
20 changes: 18 additions & 2 deletions packages/backend/src/tests/oidc-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ let nextIdToken = "";
let certDir: string | null = null;
let prevTlsRejectUnauthorized: string | undefined;

async function signIdToken(claims: Record<string, unknown>): Promise<string> {
async function signIdToken(claims: Record<string, unknown>, tokenIssuer = issuer): Promise<string> {
return new SignJWT(claims)
.setProtectedHeader({ alg: "ES256", kid: "test-kid" })
.setIssuer(issuer)
.setIssuer(tokenIssuer)
.setAudience(CLIENT_ID)
.setSubject("sub-123")
.setExpirationTime("5m")
Expand Down Expand Up @@ -241,6 +241,22 @@ describe("token exchange + id_token validation", () => {
await expect(validateIdToken(idToken, "nonce-1")).rejects.toThrow();
});

test("accepts an id_token whose issuer has a trailing slash", async () => {
resetOidcServiceForTesting();
trailingSlashIssuer = true;
try {
const idToken = await signIdToken(
{ email: "alice@example.com", email_verified: true, nonce: "nonce-1" },
`${issuer}/`,
);
const info = await validateIdToken(idToken, "nonce-1");
expect(info.email).toBe("alice@example.com");
} finally {
trailingSlashIssuer = false;
resetOidcServiceForTesting();
}
});

test("id_token with wrong audience is rejected", async () => {
const idToken = await new SignJWT({ email_verified: true })
.setProtectedHeader({ alg: "ES256", kid: "test-kid" })
Expand Down