chore(core): Support state and nonce parameter for OIDC (#19098)

This commit is contained in:
Andreas Fitzek
2025-09-03 12:33:56 +02:00
committed by GitHub
parent 52747b1625
commit 06578f287c
5 changed files with 297 additions and 41 deletions

View File

@@ -52,6 +52,8 @@ export const RESPONSE_ERROR_MESSAGES = {
} as const;
export const AUTH_COOKIE_NAME = 'n8n-auth';
export const OIDC_STATE_COOKIE_NAME = 'n8n-oidc-state';
export const OIDC_NONCE_COOKIE_NAME = 'n8n-oidc-nonce';
export const NPM_COMMAND_TOKENS = {
NPM_PACKAGE_NOT_FOUND_ERROR: '404 Not Found',

View File

@@ -11,6 +11,7 @@ import {
UserRepository,
} from '@n8n/db';
import { Container, Service } from '@n8n/di';
import { randomUUID } from 'crypto';
import { Cipher } from 'n8n-core';
import { jsonParse, UserError } from 'n8n-workflow';
import * as client from 'openid-client';
@@ -18,15 +19,16 @@ import * as client from 'openid-client';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { ForbiddenError } from '@/errors/response-errors/forbidden.error';
import { InternalServerError } from '@/errors/response-errors/internal-server.error';
import { JwtService } from '@/services/jwt.service';
import { UrlService } from '@/services/url.service';
import { OIDC_CLIENT_SECRET_REDACTED_VALUE, OIDC_PREFERENCES_DB_KEY } from './constants';
import {
getCurrentAuthenticationMethod,
isEmailCurrentAuthenticationMethod,
isOidcCurrentAuthenticationMethod,
setCurrentAuthenticationMethod,
} from '../sso-helpers';
import { OIDC_CLIENT_SECRET_REDACTED_VALUE, OIDC_PREFERENCES_DB_KEY } from './constants';
const DEFAULT_OIDC_CONFIG: OidcConfigDto = {
clientId: '',
@@ -56,6 +58,7 @@ export class OidcService {
private readonly userRepository: UserRepository,
private readonly cipher: Cipher,
private readonly logger: Logger,
private readonly jwtService: JwtService,
) {}
async init() {
@@ -76,23 +79,116 @@ export class OidcService {
};
}
async generateLoginUrl(): Promise<URL> {
generateState() {
const state = `n8n_state:${randomUUID()}`;
return {
signed: this.jwtService.sign({ state }, { expiresIn: '15m' }),
plaintext: state,
};
}
verifyState(signedState: string) {
let state: string;
try {
const decodedState = this.jwtService.verify(signedState);
state = decodedState?.state;
} catch (error) {
this.logger.error('Failed to verify state', { error });
throw new BadRequestError('Invalid state');
}
if (typeof state !== 'string') {
this.logger.error('Provided state has an invalid format');
throw new BadRequestError('Invalid state');
}
const splitState = state.split(':');
if (splitState.length !== 2 || splitState[0] !== 'n8n_state') {
this.logger.error('Provided state is missing the well-known prefix');
throw new BadRequestError('Invalid state');
}
if (
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
splitState[1],
)
) {
this.logger.error('Provided state is not formatted correctly');
throw new BadRequestError('Invalid state');
}
return state;
}
generateNonce() {
const nonce = `n8n_nonce:${randomUUID()}`;
return {
signed: this.jwtService.sign({ nonce }, { expiresIn: '15m' }),
plaintext: nonce,
};
}
verifyNonce(signedNonce: string) {
let nonce: string;
try {
const decodedNonce = this.jwtService.verify(signedNonce);
nonce = decodedNonce?.nonce;
} catch (error) {
this.logger.error('Failed to verify nonce', { error });
throw new BadRequestError('Invalid nonce');
}
if (typeof nonce !== 'string') {
this.logger.error('Provided nonce has an invalid format');
throw new BadRequestError('Invalid nonce');
}
const splitNonce = nonce.split(':');
if (splitNonce.length !== 2 || splitNonce[0] !== 'n8n_nonce') {
this.logger.error('Provided nonce is missing the well-known prefix');
throw new BadRequestError('Invalid nonce');
}
if (
!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(
splitNonce[1],
)
) {
this.logger.error('Provided nonce is not formatted correctly');
throw new BadRequestError('Invalid nonce');
}
return nonce;
}
async generateLoginUrl(): Promise<{ url: URL; state: string; nonce: string }> {
const configuration = await this.getOidcConfiguration();
const state = this.generateState();
const nonce = this.generateNonce();
const authorizationURL = client.buildAuthorizationUrl(configuration, {
redirect_uri: this.getCallbackUrl(),
response_type: 'code',
scope: 'openid email profile',
prompt: 'select_account',
state: state.plaintext,
nonce: nonce.plaintext,
});
return authorizationURL;
return { url: authorizationURL, state: state.signed, nonce: nonce.signed };
}
async loginUser(callbackUrl: URL): Promise<User> {
async loginUser(callbackUrl: URL, storedState: string, storedNonce: string): Promise<User> {
const configuration = await this.getOidcConfiguration();
const tokens = await client.authorizationCodeGrant(configuration, callbackUrl);
const expectedState = this.verifyState(storedState);
const expectedNonce = this.verifyNonce(storedNonce);
const tokens = await client.authorizationCodeGrant(configuration, callbackUrl, {
expectedState,
expectedNonce,
});
const claims = tokens.claims();

View File

@@ -1,18 +1,24 @@
import type { Logger } from '@n8n/backend-common';
import type { GlobalConfig } from '@n8n/config';
import { Time } from '@n8n/constants';
import { GLOBAL_MEMBER_ROLE, type User } from '@n8n/db';
import { type Request, type Response } from 'express';
import { mock } from 'jest-mock-extended';
import type { OidcService } from '../../oidc.service.ee';
import { OidcController } from '../oidc.controller.ee';
import type { AuthService } from '@/auth/auth.service';
import { OIDC_NONCE_COOKIE_NAME, OIDC_STATE_COOKIE_NAME } from '@/constants';
import type { AuthlessRequest } from '@/requests';
import type { UrlService } from '@/services/url.service';
import type { OidcService } from '../../oidc.service.ee';
import { OidcController } from '../oidc.controller.ee';
const authService = mock<AuthService>();
const oidcService = mock<OidcService>();
const urlService = mock<UrlService>();
const controller = new OidcController(oidcService, authService, urlService);
const globalConfig = mock<GlobalConfig>();
const logger = mock<Logger>();
const controller = new OidcController(oidcService, authService, urlService, globalConfig, logger);
const user = mock<User>({
id: '456',
@@ -37,6 +43,10 @@ describe('OidcController', () => {
const req = mock<AuthlessRequest>({
originalUrl: '/sso/oidc/callback?code=auth_code&state=state_value',
browserId: 'browser-id-123',
cookies: {
[OIDC_STATE_COOKIE_NAME]: 'state_value',
[OIDC_NONCE_COOKIE_NAME]: 'nonce_value',
},
});
const res = mock<Response>();
@@ -50,7 +60,11 @@ describe('OidcController', () => {
await controller.callbackHandler(req, res);
// Verify that loginUser was called with the correct callback URL
expect(oidcService.loginUser).toHaveBeenCalledWith(expectedCallbackUrl);
expect(oidcService.loginUser).toHaveBeenCalledWith(
expectedCallbackUrl,
'state_value',
'nonce_value',
);
// Verify that issueCookie was called with MFA flag set to true
expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, req.browserId);
@@ -64,6 +78,10 @@ describe('OidcController', () => {
originalUrl:
'/sso/oidc/callback?code=different_code&state=different_state&session_state=session123',
browserId: 'browser-id-123',
cookies: {
[OIDC_STATE_COOKIE_NAME]: 'state_value',
[OIDC_NONCE_COOKIE_NAME]: 'nonce_value',
},
});
const res = mock<Response>();
@@ -75,7 +93,11 @@ describe('OidcController', () => {
await controller.callbackHandler(req, res);
expect(oidcService.loginUser).toHaveBeenCalledWith(expectedCallbackUrl);
expect(oidcService.loginUser).toHaveBeenCalledWith(
expectedCallbackUrl,
'state_value',
'nonce_value',
);
expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, req.browserId);
expect(res.redirect).toHaveBeenCalledWith('/');
});
@@ -84,6 +106,10 @@ describe('OidcController', () => {
const req = mock<AuthlessRequest>({
originalUrl: '/sso/oidc/callback',
browserId: undefined,
cookies: {
[OIDC_STATE_COOKIE_NAME]: 'state_value',
[OIDC_NONCE_COOKIE_NAME]: 'nonce_value',
},
});
const res = mock<Response>();
@@ -93,7 +119,11 @@ describe('OidcController', () => {
await controller.callbackHandler(req, res);
expect(oidcService.loginUser).toHaveBeenCalledWith(expectedCallbackUrl);
expect(oidcService.loginUser).toHaveBeenCalledWith(
expectedCallbackUrl,
'state_value',
'nonce_value',
);
expect(authService.issueCookie).toHaveBeenCalledWith(res, user, true, undefined);
expect(res.redirect).toHaveBeenCalledWith('/');
});
@@ -101,6 +131,10 @@ describe('OidcController', () => {
test('Should propagate errors from OIDC service', async () => {
const req = mock<Request>({
originalUrl: '/sso/oidc/callback?code=auth_code&state=state_value',
cookies: {
[OIDC_STATE_COOKIE_NAME]: 'state_value',
[OIDC_NONCE_COOKIE_NAME]: 'nonce_value',
},
});
const res = mock<Response>();
@@ -119,11 +153,16 @@ describe('OidcController', () => {
test('Should redirect to generated authorization URL', async () => {
const req = mock<Request>();
const res = mock<Response>();
globalConfig.auth.cookie = { samesite: 'lax', secure: true };
const mockAuthUrl = new URL(
'https://provider.com/auth?client_id=123&redirect_uri=http://localhost:5678/callback',
);
oidcService.generateLoginUrl.mockResolvedValueOnce(mockAuthUrl);
oidcService.generateLoginUrl.mockResolvedValueOnce({
url: mockAuthUrl,
state: 'state_value',
nonce: 'nonce_value',
});
await controller.redirectToAuthProvider(req, res);
@@ -131,6 +170,18 @@ describe('OidcController', () => {
expect(res.redirect).toHaveBeenCalledWith(
'https://provider.com/auth?client_id=123&redirect_uri=http://localhost:5678/callback',
);
expect(res.cookie).toHaveBeenCalledWith(OIDC_STATE_COOKIE_NAME, 'state_value', {
httpOnly: true,
sameSite: 'lax',
secure: true,
maxAge: 15 * Time.minutes.toMilliseconds,
});
expect(res.cookie).toHaveBeenCalledWith(OIDC_NONCE_COOKIE_NAME, 'nonce_value', {
httpOnly: true,
sameSite: 'lax',
secure: true,
maxAge: 15 * Time.minutes.toMilliseconds,
});
});
test('Should propagate errors from OIDC service during URL generation', async () => {

View File

@@ -1,14 +1,19 @@
import { OidcConfigDto } from '@n8n/api-types';
import { Logger } from '@n8n/backend-common';
import { GlobalConfig } from '@n8n/config';
import { Time } from '@n8n/constants';
import { AuthenticatedRequest } from '@n8n/db';
import { Body, Get, GlobalScope, Licensed, Post, RestController } from '@n8n/decorators';
import { Request, Response } from 'express';
import { AuthService } from '@/auth/auth.service';
import { OIDC_NONCE_COOKIE_NAME, OIDC_STATE_COOKIE_NAME } from '@/constants';
import { BadRequestError } from '@/errors/response-errors/bad-request.error';
import { AuthlessRequest } from '@/requests';
import { UrlService } from '@/services/url.service';
import { OIDC_CLIENT_SECRET_REDACTED_VALUE } from '../constants';
import { OidcService } from '../oidc.service.ee';
import { AuthlessRequest } from '@/requests';
@RestController('/sso/oidc')
export class OidcController {
@@ -16,6 +21,8 @@ export class OidcController {
private readonly oidcService: OidcService,
private readonly authService: AuthService,
private readonly urlService: UrlService,
private readonly globalConfig: GlobalConfig,
private readonly logger: Logger,
) {}
@Get('/config')
@@ -45,9 +52,22 @@ export class OidcController {
@Get('/login', { skipAuth: true })
@Licensed('feat:oidc')
async redirectToAuthProvider(_req: Request, res: Response) {
const authorizationURL = await this.oidcService.generateLoginUrl();
const authorization = await this.oidcService.generateLoginUrl();
const { samesite, secure } = this.globalConfig.auth.cookie;
res.redirect(authorizationURL.toString());
res.cookie(OIDC_STATE_COOKIE_NAME, authorization.state, {
maxAge: 15 * Time.minutes.toMilliseconds,
httpOnly: true,
sameSite: samesite,
secure,
});
res.cookie(OIDC_NONCE_COOKIE_NAME, authorization.nonce, {
maxAge: 15 * Time.minutes.toMilliseconds,
httpOnly: true,
sameSite: samesite,
secure,
});
res.redirect(authorization.url.toString());
}
@Get('/callback', { skipAuth: true })
@@ -55,9 +75,24 @@ export class OidcController {
async callbackHandler(req: AuthlessRequest, res: Response) {
const fullUrl = `${this.urlService.getInstanceBaseUrl()}${req.originalUrl}`;
const callbackUrl = new URL(fullUrl);
const state = req.cookies[OIDC_STATE_COOKIE_NAME];
const user = await this.oidcService.loginUser(callbackUrl);
if (typeof state !== 'string') {
this.logger.error('State is missing');
throw new BadRequestError('Invalid state');
}
const nonce = req.cookies[OIDC_NONCE_COOKIE_NAME];
if (typeof nonce !== 'string') {
this.logger.error('Nonce is missing');
throw new BadRequestError('Invalid nonce');
}
const user = await this.oidcService.loginUser(callbackUrl, state, nonce);
res.clearCookie(OIDC_STATE_COOKIE_NAME);
res.clearCookie(OIDC_NONCE_COOKIE_NAME);
this.authService.issueCookie(res, user, true, req.browserId);
res.redirect('/');

View File

@@ -22,6 +22,7 @@ import { OIDC_CLIENT_SECRET_REDACTED_VALUE } from '@/sso.ee/oidc/constants';
import { OidcService } from '@/sso.ee/oidc/oidc.service.ee';
import { createUser } from '@test-integration/db/users';
import { UserError } from 'n8n-workflow';
import { JwtService } from '@/services/jwt.service';
beforeAll(async () => {
await testDb.init();
@@ -211,8 +212,8 @@ describe('OIDC service', () => {
// Generate login URL again - should use new configuration
const authUrl = await oidcService.generateLoginUrl();
expect(authUrl.pathname).toEqual('/auth');
expect(authUrl.searchParams.get('client_id')).toEqual('new-client-id');
expect(authUrl.url.pathname).toEqual('/auth');
expect(authUrl.url.searchParams.get('client_id')).toEqual('new-client-id');
// Verify discovery was called again due to cache invalidation
expect(discoveryMock).toHaveBeenCalledTimes(4); // Initial config, initial login, new config, new login
@@ -248,19 +249,26 @@ describe('OIDC service', () => {
const authUrl = await oidcService.generateLoginUrl();
expect(authUrl.pathname).toEqual('/auth');
expect(authUrl.searchParams.get('client_id')).toEqual('test-client-id');
expect(authUrl.searchParams.get('redirect_uri')).toEqual(
expect(authUrl.url.pathname).toEqual('/auth');
expect(authUrl.url.searchParams.get('client_id')).toEqual('test-client-id');
expect(authUrl.url.searchParams.get('redirect_uri')).toEqual(
'http://localhost:5678/rest/sso/oidc/callback',
);
expect(authUrl.searchParams.get('response_type')).toEqual('code');
expect(authUrl.searchParams.get('scope')).toEqual('openid email profile');
expect(authUrl.url.searchParams.get('response_type')).toEqual('code');
expect(authUrl.url.searchParams.get('scope')).toEqual('openid email profile');
expect(authUrl.url.searchParams.get('state')).toBeDefined();
expect(authUrl.url.searchParams.get('state')?.startsWith('n8n_state:')).toBe(true);
expect(authUrl.state).toBeDefined();
expect(authUrl.nonce).toBeDefined();
});
describe('loginUser', () => {
it('should handle new user login with valid callback URL', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -288,7 +296,7 @@ describe('OIDC service', () => {
email: 'user2@example.com',
});
const user = await oidcService.loginUser(callbackUrl);
const user = await oidcService.loginUser(callbackUrl, state.signed, nonce.signed);
expect(user).toBeDefined();
expect(user.email).toEqual('user2@example.com');
@@ -303,8 +311,10 @@ describe('OIDC service', () => {
});
it('should handle existing user login with valid callback URL', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -324,7 +334,7 @@ describe('OIDC service', () => {
expiresIn: () => 3600,
} as mocked_oidc_client.TokenEndpointResponse &
mocked_oidc_client.TokenEndpointResponseHelpers;
state;
authorizationCodeGrantMock.mockResolvedValueOnce(mockTokens);
fetchUserInfoMock.mockResolvedValueOnce({
@@ -332,15 +342,17 @@ describe('OIDC service', () => {
email: 'user2@example.com',
});
const user = await oidcService.loginUser(callbackUrl);
const user = await oidcService.loginUser(callbackUrl, state.signed, nonce.signed);
expect(user).toBeDefined();
expect(user.email).toEqual('user2@example.com');
expect(user.id).toEqual(createdUser.id);
});
it('should sign up the user if user already exists out of OIDC system', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -369,14 +381,16 @@ describe('OIDC service', () => {
email: 'user1@example.com',
});
const user = await oidcService.loginUser(callbackUrl);
const user = await oidcService.loginUser(callbackUrl, state.signed, nonce.signed);
expect(user).toBeDefined();
expect(user.email).toEqual('user1@example.com');
});
it('should sign in user if OIDC Idp does not have email verified', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -405,14 +419,16 @@ describe('OIDC service', () => {
email: 'user3@example.com',
});
const user = await oidcService.loginUser(callbackUrl);
const user = await oidcService.loginUser(callbackUrl, state.signed, nonce.signed);
expect(user).toBeDefined();
expect(user.email).toEqual('user3@example.com');
});
it('should throw `BadRequestError` if OIDC Idp does not provide an email', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -440,12 +456,16 @@ describe('OIDC service', () => {
email_verified: true,
});
await expect(oidcService.loginUser(callbackUrl)).rejects.toThrowError(BadRequestError);
await expect(
oidcService.loginUser(callbackUrl, state.signed, nonce.signed),
).rejects.toThrowError(BadRequestError);
});
it('should throw `BadRequestError` if OIDC Idp provides an invalid email format', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -474,7 +494,9 @@ describe('OIDC service', () => {
email: 'invalid-email-format',
});
const error = await oidcService.loginUser(callbackUrl).catch((e) => e);
const error = await oidcService
.loginUser(callbackUrl, state.signed, nonce.signed)
.catch((e) => e);
expect(error.message).toBe('Invalid email format');
});
@@ -485,8 +507,10 @@ describe('OIDC service', () => {
['spaces in@email.com'],
['double@@domain.com'],
])('should throw `BadRequestError` for invalid email <%s>', async (invalidEmail) => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -513,12 +537,16 @@ describe('OIDC service', () => {
email: invalidEmail,
});
await expect(oidcService.loginUser(callbackUrl)).rejects.toThrowError(BadRequestError);
await expect(
oidcService.loginUser(callbackUrl, state.signed, nonce.signed),
).rejects.toThrowError(BadRequestError);
});
it('should throw `ForbiddenError` if OIDC token does not provide claims', async () => {
const state = oidcService.generateState();
const nonce = oidcService.generateNonce();
const callbackUrl = new URL(
'http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=valid-state',
`http://localhost:5678/rest/sso/oidc/callback?code=valid-code&state=${state.plaintext}`,
);
const mockTokens: mocked_oidc_client.TokenEndpointResponse &
@@ -540,7 +568,51 @@ describe('OIDC service', () => {
email_verified: true,
});
await expect(oidcService.loginUser(callbackUrl)).rejects.toThrowError(ForbiddenError);
await expect(
oidcService.loginUser(callbackUrl, state.signed, nonce.signed),
).rejects.toThrowError(ForbiddenError);
});
});
describe('State and nonce', () => {
it('should generate and verify a valid state', () => {
const state = oidcService.generateState();
const decoded = oidcService.verifyState(state.signed);
expect(decoded).toBe(state.plaintext);
});
it('should generate and verify a valid nonce', () => {
const nonce = oidcService.generateNonce();
const decoded = oidcService.verifyNonce(nonce.signed);
expect(decoded).toBe(nonce.plaintext);
});
it('should throw an error for an invalid state', () => {
expect(() => oidcService.verifyState('invalid_state')).toThrow(BadRequestError);
});
it('should throw an error for an invalid formatted state', () => {
const invalid = Container.get(JwtService).sign({ state: 'invalid_state' });
expect(() => oidcService.verifyState(invalid)).toThrow(BadRequestError);
});
it('should throw an error for an invalid random part of the state', () => {
const invalid = Container.get(JwtService).sign({ state: 'n8n_state:invalid-state' });
expect(() => oidcService.verifyState(invalid)).toThrow(BadRequestError);
});
it('should throw an error for an invalid nonce', () => {
expect(() => oidcService.verifyNonce('invalid_nonce')).toThrow(BadRequestError);
});
it('should throw an error for an invalid formatted nonce', () => {
const invalid = Container.get(JwtService).sign({ nonce: 'invalid_nonce' });
expect(() => oidcService.verifyNonce(invalid)).toThrow(BadRequestError);
});
it('should throw an error for an invalid random part of the nonce', () => {
const invalid = Container.get(JwtService).sign({ nonce: 'n8n_nonce:invalid-nonce' });
expect(() => oidcService.verifyNonce(invalid)).toThrow(BadRequestError);
});
});
});