@@ -17,6 +17,8 @@ import { OidcService } from '../oidc.service.ee';
1717import { Publisher } from '@/scaling/pubsub/publisher.service' ;
1818import type { OidcConfigDto } from '@n8n/api-types' ;
1919import { type ProvisioningService } from '@/modules/provisioning.ee/provisioning.service.ee' ;
20+ import { BadRequestError } from '@/errors/response-errors/bad-request.error' ;
21+ import { ForbiddenError } from '@/errors/response-errors/forbidden.error' ;
2022
2123describe ( 'OidcService' , ( ) => {
2224 let oidcService : OidcService ;
@@ -27,6 +29,8 @@ describe('OidcService', () => {
2729 let logger : Logger ;
2830 let jwtService : JwtService ;
2931 let provisioningService : ProvisioningService ;
32+ let userRepository : UserRepository ;
33+ let authIdentityRepository : AuthIdentityRepository ;
3034
3135 const mockOidcConfig = {
3236 clientId : 'test-client-id' ,
@@ -59,17 +63,18 @@ describe('OidcService', () => {
5963 logger = mockLogger ( ) ;
6064 jwtService = mock < JwtService > ( ) ;
6165 provisioningService = mock < ProvisioningService > ( ) ;
62-
66+ userRepository = mock < UserRepository > ( ) ;
67+ authIdentityRepository = mock < AuthIdentityRepository > ( ) ;
6368 jest
6469 . spyOn ( ssoHelpers , 'setCurrentAuthenticationMethod' )
6570 . mockImplementation ( async ( ) => await Promise . resolve ( ) ) ;
6671
6772 oidcService = new OidcService (
6873 settingsRepository ,
69- mock < AuthIdentityRepository > ( ) ,
74+ authIdentityRepository ,
7075 mock < UrlService > ( ) ,
7176 globalConfig ,
72- mock < UserRepository > ( ) ,
77+ userRepository ,
7378 cipher ,
7479 logger ,
7580 jwtService ,
@@ -310,4 +315,226 @@ describe('OidcService', () => {
310315 expect ( mockPublisher . publishCommand ) . not . toHaveBeenCalled ( ) ;
311316 } ) ;
312317 } ) ;
318+
319+ describe ( 'loginUser' , ( ) => {
320+ it ( 'throws an error if authorizationCodeGrant throws an error' , async ( ) => {
321+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
322+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
323+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
324+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
325+ jest
326+ . spyOn ( client , 'authorizationCodeGrant' )
327+ . mockRejectedValue ( new Error ( 'Authorization code grant failed' ) ) ;
328+
329+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
330+ const storedState = oidcService . generateState ( ) . signed ;
331+ const storedNonce = oidcService . generateNonce ( ) . signed ;
332+
333+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
334+ new BadRequestError ( 'Invalid authorization code' ) ,
335+ ) ;
336+ } ) ;
337+
338+ it ( 'throws an error if claims() throws an error' , async ( ) => {
339+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
340+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
341+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
342+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
343+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
344+ access_token : 'valid-access-token' ,
345+ token_type : 'bearer' ,
346+ claims : ( ) => {
347+ throw new Error ( 'Claims extraction failed' ) ;
348+ } ,
349+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
350+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
351+ const storedState = oidcService . generateState ( ) . signed ;
352+ const storedNonce = oidcService . generateNonce ( ) . signed ;
353+
354+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
355+ new BadRequestError ( 'Invalid token' ) ,
356+ ) ;
357+ } ) ;
358+
359+ it ( 'should throw an error if there are no claims' , async ( ) => {
360+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
361+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
362+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
363+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
364+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
365+ access_token : 'valid-access-token' ,
366+ token_type : 'bearer' ,
367+ claims : ( ) => {
368+ return undefined ;
369+ } ,
370+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
371+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
372+ const storedState = oidcService . generateState ( ) . signed ;
373+ const storedNonce = oidcService . generateNonce ( ) . signed ;
374+
375+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
376+ new ForbiddenError ( 'No claims found in the OIDC token' ) ,
377+ ) ;
378+ } ) ;
379+
380+ it ( 'throws an error if fetchUserInfo throws an error' , async ( ) => {
381+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
382+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
383+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
384+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
385+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
386+ access_token : 'valid-access-token' ,
387+ token_type : 'bearer' ,
388+ claims : ( ) => {
389+ return { sub : 'valid-subject' } ;
390+ } ,
391+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
392+ jest . spyOn ( client , 'fetchUserInfo' ) . mockRejectedValue ( new Error ( 'Fetch user info failed' ) ) ;
393+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
394+ const storedState = oidcService . generateState ( ) . signed ;
395+ const storedNonce = oidcService . generateNonce ( ) . signed ;
396+
397+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
398+ new BadRequestError ( 'Invalid token' ) ,
399+ ) ;
400+ } ) ;
401+
402+ it ( 'throws an error if there is no email' , async ( ) => {
403+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
404+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
405+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
406+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
407+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
408+ access_token : 'valid-access-token' ,
409+ token_type : 'bearer' ,
410+ claims : ( ) => {
411+ return { sub : 'valid-subject' } ;
412+ } ,
413+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
414+ jest . spyOn ( client , 'fetchUserInfo' ) . mockResolvedValue ( { email_verified : true } as any ) ;
415+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
416+ const storedState = oidcService . generateState ( ) . signed ;
417+ const storedNonce = oidcService . generateNonce ( ) . signed ;
418+
419+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
420+ new BadRequestError ( 'An email is required' ) ,
421+ ) ;
422+ } ) ;
423+
424+ it ( 'throws an error if the email is invalid' , async ( ) => {
425+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
426+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
427+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
428+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
429+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
430+ access_token : 'valid-access-token' ,
431+ token_type : 'bearer' ,
432+ claims : ( ) => {
433+ return { sub : 'valid-subject' } ;
434+ } ,
435+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
436+ jest
437+ . spyOn ( client , 'fetchUserInfo' )
438+ . mockResolvedValue ( { email_verified : true , email : 'invalid-email' } as any ) ;
439+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
440+ const storedState = oidcService . generateState ( ) . signed ;
441+ const storedNonce = oidcService . generateNonce ( ) . signed ;
442+
443+ await expect ( oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ) . rejects . toThrow (
444+ new BadRequestError ( 'Invalid email format' ) ,
445+ ) ;
446+ } ) ;
447+
448+ it ( 'should return the user if the auth identity already exists' , async ( ) => {
449+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
450+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
451+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
452+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
453+ // @ts -expect-error - applySsoProvisioning is private and only accessible within class 'OidcService'
454+ oidcService . applySsoProvisioning = jest . fn ( ) . mockResolvedValue ( undefined ) ;
455+ authIdentityRepository . findOne = jest
456+ . fn ( )
457+ . mockResolvedValue ( { user :
{ email :
'[email protected] ' } as any } ) ; 458+
459+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
460+ access_token : 'valid-access-token' ,
461+ token_type : 'bearer' ,
462+ claims : ( ) => {
463+ return { sub : 'valid-subject' } ;
464+ } ,
465+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
466+ jest
467+ . spyOn ( client , 'fetchUserInfo' )
468+ . mockResolvedValue ( { email_verified :
true , email :
'[email protected] ' } as any ) ; 469+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
470+ const storedState = oidcService . generateState ( ) . signed ;
471+ const storedNonce = oidcService . generateNonce ( ) . signed ;
472+
473+ const user = await oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ;
474+ expect ( user ) . toBeDefined ( ) ;
475+ expect ( user . email ) . toEqual ( '[email protected] ' ) ; 476+ // @ts -expect-error - applySsoProvisioning is private and only accessible within class 'OidcService'
477+ expect ( oidcService . applySsoProvisioning ) . toHaveBeenCalledWith ( user , { sub : 'valid-subject' } ) ;
478+ } ) ;
479+
480+ it ( 'should return a user if the user exists but the auth identity does not' , async ( ) => {
481+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
482+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
483+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
484+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
485+ // @ts -expect-error - applySsoProvisioning is private and only accessible within class 'OidcService'
486+ oidcService . applySsoProvisioning = jest . fn ( ) . mockResolvedValue ( undefined ) ;
487+ userRepository . findOne = jest . fn ( ) . mockResolvedValue ( { email :
'[email protected] ' } as any ) ; 488+
489+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
490+ access_token : 'valid-access-token' ,
491+ token_type : 'bearer' ,
492+ claims : ( ) => {
493+ return { sub : 'valid-subject' } ;
494+ } ,
495+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
496+ jest
497+ . spyOn ( client , 'fetchUserInfo' )
498+ . mockResolvedValue ( { email_verified :
true , email :
'[email protected] ' } as any ) ; 499+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
500+ const storedState = oidcService . generateState ( ) . signed ;
501+ const storedNonce = oidcService . generateNonce ( ) . signed ;
502+
503+ const user = await oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ;
504+ expect ( user ) . toBeDefined ( ) ;
505+ expect ( user . email ) . toEqual ( '[email protected] ' ) ; 506+ // @ts -expect-error - applySsoProvisioning is private and only accessible within class 'OidcService'
507+ expect ( oidcService . applySsoProvisioning ) . toHaveBeenCalledWith ( user , { sub : 'valid-subject' } ) ;
508+ } ) ;
509+
510+ it ( 'should create a new user if the user does not exist' , async ( ) => {
511+ oidcService . verifyState = jest . fn ( ) . mockReturnValue ( 'valid-state' ) ;
512+ oidcService . verifyNonce = jest . fn ( ) . mockReturnValue ( 'valid-nonce' ) ;
513+ // @ts -expect-error - getOidcConfiguration is private and only accessible within class 'OidcService'
514+ oidcService . getOidcConfiguration = jest . fn ( ) . mockResolvedValue ( { } as client . Configuration ) ;
515+ // @ts -expect-error - applySsoProvisioning is private and only accessible within class 'OidcService'
516+ oidcService . applySsoProvisioning = jest . fn ( ) . mockResolvedValue ( undefined ) ;
517+ userRepository . manager . transaction = jest
518+ . fn ( )
519+ . mockResolvedValue ( { email :
'[email protected] ' } as any ) ; 520+
521+ jest . spyOn ( client , 'authorizationCodeGrant' ) . mockResolvedValue ( {
522+ access_token : 'valid-access-token' ,
523+ token_type : 'bearer' ,
524+ claims : ( ) => {
525+ return { sub : 'valid-subject' } ;
526+ } ,
527+ } as unknown as client . TokenEndpointResponse & client . TokenEndpointResponseHelpers ) ;
528+ jest
529+ . spyOn ( client , 'fetchUserInfo' )
530+ . mockResolvedValue ( { email_verified :
true , email :
'[email protected] ' } as any ) ; 531+ const callbackUrl = new URL ( 'https://example.com/callback' ) ;
532+ const storedState = oidcService . generateState ( ) . signed ;
533+ const storedNonce = oidcService . generateNonce ( ) . signed ;
534+
535+ const user = await oidcService . loginUser ( callbackUrl , storedState , storedNonce ) ;
536+ expect ( user ) . toBeDefined ( ) ;
537+ expect ( user . email ) . toEqual ( '[email protected] ' ) ; 538+ } ) ;
539+ } ) ;
313540} ) ;
0 commit comments