Add nonce for inclusion in id_token

Signed-off-by: JP Dhabolt <john.p.dhabolt@gmail.com>
This commit is contained in:
JP Dhabolt
2024-04-17 14:54:09 -04:00
parent 26b660a3ac
commit 7668ddc9ad
3 changed files with 67 additions and 33 deletions
@@ -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<string, boolean> = {};
@@ -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,
@@ -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<string, string> = {
scope: input.scope || initializedScope || 'openid profile email',
state: input.state,
nonce: crypto.randomBytes(16).toString('base64'),
};
const prompt = initializedPrompt || 'none';
if (prompt !== 'auto') {
@@ -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({