diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts index eb6389f48b..19628f6def 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.test.ts @@ -32,6 +32,7 @@ import express from 'express'; describe('oidcAuthenticator', () => { let implementation: any; let oauthState: OAuthState; + let nonce: string; let idToken: string; let publicKey: JWK; const revokedTokenMap: Record = {}; @@ -65,22 +66,7 @@ describe('oidcAuthenticator', () => { request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; - beforeAll(async () => { - const keyPair = await generateKeyPair('RS256'); - const privateKey = await exportJWK(keyPair.privateKey); - publicKey = await exportJWK(keyPair.publicKey); - publicKey.alg = privateKey.alg = 'RS256'; - - idToken = await new SignJWT({ - sub: 'test', - iss: 'https://oidc.test', - iat: Date.now(), - aud: 'clientId', - exp: Date.now() + 10000, - }) - .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) - .sign(keyPair.privateKey); - }); + beforeAll(async () => {}); beforeEach(() => { mswServer.use( @@ -96,6 +82,14 @@ describe('oidcAuthenticator', () => { rest.get('https://oidc.test/jwks.json', async (_req, res, ctx) => res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })), ), + rest.get( + 'https://oidc.test/oauth2/authorize', + async (req, _res, _ctx) => { + nonce = + new URL(req.url).searchParams.get('nonce') ?? + 'nonceGeneratedByAuthServer'; + }, + ), rest.post('https://oidc.test/oauth2/token', async (req, res, ctx) => { const formBody = new URLSearchParams(await req.text()); if ( @@ -104,6 +98,23 @@ describe('oidcAuthenticator', () => { ) { return res(ctx.json({})); } + + const keyPair = await generateKeyPair('RS256'); + const privateKey = await exportJWK(keyPair.privateKey); + publicKey = await exportJWK(keyPair.publicKey); + publicKey.alg = privateKey.alg = 'RS256'; + + idToken = await new SignJWT({ + sub: 'test', + iss: 'https://oidc.test', + iat: Date.now(), + aud: 'clientId', + exp: Date.now() + 10000, + nonce, + }) + .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) + .sign(keyPair.privateKey); + return res( req.headers.get('Authorization') ? ctx.json({ @@ -198,6 +209,15 @@ describe('oidcAuthenticator', () => { expect(searchParams.get('response_type')).toBe('code'); }); + it('passes a nonce', async () => { + const startResponse = await oidcAuthenticator.start( + startRequest, + implementation, + ); + const { searchParams } = new URL(startResponse.url); + expect(searchParams.get('nonce')).not.toBeNull(); + }); + it('passes client ID from config', async () => { const startResponse = await oidcAuthenticator.start( startRequest, diff --git a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts index 317cb7269e..f954d228e2 100644 --- a/plugins/auth-backend-module-oidc-provider/src/authenticator.ts +++ b/plugins/auth-backend-module-oidc-provider/src/authenticator.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import crypto from 'crypto'; import { custom, CustomHttpOptionsProvider, @@ -131,6 +132,7 @@ export const oidcAuthenticator = createOAuthAuthenticator({ const options: Record = { scope: input.scope || initializedScope || 'openid profile email', state: input.state, + nonce: crypto.randomBytes(16).toString('base64'), }; const prompt = initializedPrompt || 'none'; if (prompt !== 'auto') { diff --git a/plugins/auth-backend-module-oidc-provider/src/module.test.ts b/plugins/auth-backend-module-oidc-provider/src/module.test.ts index 216c101049..ad377e0769 100644 --- a/plugins/auth-backend-module-oidc-provider/src/module.test.ts +++ b/plugins/auth-backend-module-oidc-provider/src/module.test.ts @@ -30,6 +30,7 @@ import { authModuleOidcProvider } from './module'; describe('authModuleOidcProvider', () => { let backstageServer: Server; let appUrl: string; + let nonce: string; let idToken: string; let publicKey: JWK; @@ -56,22 +57,7 @@ describe('authModuleOidcProvider', () => { request_object_signing_alg_values_supported: ['RS256', 'RS512', 'HS256'], }; - beforeAll(async () => { - const keyPair = await generateKeyPair('RS256'); - const privateKey = await exportJWK(keyPair.privateKey); - publicKey = await exportJWK(keyPair.publicKey); - publicKey.alg = privateKey.alg = 'RS256'; - - idToken = await new SignJWT({ - sub: 'test', - iss: 'https://oidc.test', - iat: Date.now(), - aud: 'clientId', - exp: Date.now() + 10000, - }) - .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) - .sign(keyPair.privateKey); - }); + beforeAll(async () => {}); beforeEach(async () => { jest.clearAllMocks(); @@ -86,6 +72,14 @@ describe('authModuleOidcProvider', () => { ctx.json(issuerMetadata), ), ), + rest.get( + 'https://oidc.test/oauth2/authorize', + async (req, _res, _ctx) => { + nonce = + new URL(req.url).searchParams.get('nonce') ?? + 'nonceGeneratedByAuthServer'; + }, + ), rest.get('https://oidc.test/oauth2/authorize', async (req, res, ctx) => { const callbackUrl = new URL(req.url.searchParams.get('redirect_uri')!); callbackUrl.searchParams.set('code', 'authorization_code'); @@ -103,6 +97,22 @@ describe('authModuleOidcProvider', () => { res(ctx.status(200), ctx.json({ keys: [{ ...publicKey }] })), ), rest.post('https://oidc.test/oauth2/token', async (req, res, ctx) => { + const keyPair = await generateKeyPair('RS256'); + const privateKey = await exportJWK(keyPair.privateKey); + publicKey = await exportJWK(keyPair.publicKey); + publicKey.alg = privateKey.alg = 'RS256'; + + idToken = await new SignJWT({ + sub: 'test', + iss: 'https://oidc.test', + iat: Date.now(), + aud: 'clientId', + exp: Date.now() + 10000, + nonce, + }) + .setProtectedHeader({ alg: privateKey.alg, kid: privateKey.kid }) + .sign(keyPair.privateKey); + return res( req.headers.get('Authorization') ? ctx.json({ @@ -187,7 +197,8 @@ describe('authModuleOidcProvider', () => { const startUrl = new URL(startResponse.get('location')); expect(startUrl.origin).toBe('https://oidc.test'); expect(startUrl.pathname).toBe('/oauth2/authorize'); - expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + const expected = Object.fromEntries(startUrl.searchParams); + expect(expected).toEqual({ response_type: 'code', scope: 'openid profile email', client_id: 'clientId', @@ -196,6 +207,7 @@ describe('authModuleOidcProvider', () => { prompt: 'none', code_challenge: expect.any(String), code_challenge_method: `S256`, + nonce: expected.nonce, }); expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({