mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-21 11:49:59 +00:00
chore(core): Support state and nonce parameter for OIDC (#19098)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user