From 6b55c9b97e0cf0976073c62a9f55dd14a1c9504a Mon Sep 17 00:00:00 2001 From: dankelleher Date: Thu, 5 Jun 2025 08:52:30 +0200 Subject: [PATCH] Add oidc support - if the scope includes "openid", an id token will be returned from the auth server. This change does not enforce this, but simply does not throw away the id token if it is present. --- src/server/auth/handlers/token.test.ts | 56 +++++++++++++++++--------- src/shared/auth.ts | 1 + 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/src/server/auth/handlers/token.test.ts b/src/server/auth/handlers/token.test.ts index 4b7fae025..946cc6910 100644 --- a/src/server/auth/handlers/token.test.ts +++ b/src/server/auth/handlers/token.test.ts @@ -16,6 +16,18 @@ jest.mock('pkce-challenge', () => ({ }) })); +const mockTokens = { + access_token: 'mock_access_token', + token_type: 'bearer', + expires_in: 3600, + refresh_token: 'mock_refresh_token' +}; + +const mockTokensWithIdToken = { + ...mockTokens, + id_token: 'mock_id_token' +} + describe('Token Handler', () => { // Mock client data const validClient: OAuthClientInformationFull = { @@ -58,12 +70,7 @@ describe('Token Handler', () => { async exchangeAuthorizationCode(client: OAuthClientInformationFull, authorizationCode: string): Promise { if (authorizationCode === 'valid_code') { - return { - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }; + return mockTokens; } throw new InvalidGrantError('The authorization code is invalid or has expired'); }, @@ -291,18 +298,36 @@ describe('Token Handler', () => { ); }); + it('returns id token in code exchange if provided', async () => { + mockProvider.exchangeAuthorizationCode = async (client: OAuthClientInformationFull, authorizationCode: string): Promise => { + if (authorizationCode === 'valid_code') { + return mockTokensWithIdToken; + } + throw new InvalidGrantError('The authorization code is invalid or has expired'); + }; + + const response = await supertest(app) + .post('/token') + .type('form') + .send({ + client_id: 'valid-client', + client_secret: 'valid-secret', + grant_type: 'authorization_code', + code: 'valid_code', + code_verifier: 'valid_verifier' + }); + + expect(response.status).toBe(200); + expect(response.body.id_token).toBe('mock_id_token'); + }); + it('passes through code verifier when using proxy provider', async () => { const originalFetch = global.fetch; try { global.fetch = jest.fn().mockResolvedValue({ ok: true, - json: () => Promise.resolve({ - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }) + json: () => Promise.resolve(mockTokens) }); const proxyProvider = new ProxyOAuthServerProvider({ @@ -359,12 +384,7 @@ describe('Token Handler', () => { try { global.fetch = jest.fn().mockResolvedValue({ ok: true, - json: () => Promise.resolve({ - access_token: 'mock_access_token', - token_type: 'bearer', - expires_in: 3600, - refresh_token: 'mock_refresh_token' - }) + json: () => Promise.resolve(mockTokens) }); const proxyProvider = new ProxyOAuthServerProvider({ diff --git a/src/shared/auth.ts b/src/shared/auth.ts index 65b800e79..3438d6ea8 100644 --- a/src/shared/auth.ts +++ b/src/shared/auth.ts @@ -62,6 +62,7 @@ export const OAuthMetadataSchema = z export const OAuthTokensSchema = z .object({ access_token: z.string(), + id_token: z.string().optional(), // Optional for OAuth 2.1, but necessary in OpenID Connect token_type: z.string(), expires_in: z.number().optional(), scope: z.string().optional(),