@@ -20,6 +20,7 @@ describe('AuthController', () => {
2020
2121 beforeEach ( async ( ) => {
2222 jest . resetAllMocks ( ) ;
23+ ( responseMock . redirect as any ) = jest . fn ( ( href : string ) => href ) ;
2324
2425 const module : TestingModule = await Test . createTestingModule ( {
2526 imports : [ PortalModule . create ( { } ) ] ,
@@ -32,69 +33,91 @@ describe('AuthController', () => {
3233 . useValue ( cookiesServiceMock )
3334 . compile ( ) ;
3435 controller = module . get < AuthController > ( AuthController ) ;
36+ process . env . BASE_DOMAINS_DEFAULT = 'localhost' ;
3537 } ) ;
3638
3739 it ( 'should be defined' , ( ) => {
3840 expect ( controller ) . toBeDefined ( ) ;
3941 } ) ;
4042
4143 describe ( 'auth' , ( ) => {
42- it ( 'should get the config for tenant' , async ( ) => {
43- // arrange
44- const callback = jest . spyOn ( authCallbackMock , 'handleSuccess' ) ;
45- const getTokenForCode = jest . spyOn (
46- authTokenServiceMock ,
47- 'exchangeTokenForCode' ,
48- ) ;
49- requestMock . query = { code : 'foo' } ;
50- const idToken = 'id_token' ;
44+ it ( 'redirects to decoded state URL after successful token exchange' , async ( ) => {
45+ const decodedUrl = 'http://sub.localhost:4300/' ;
46+ requestMock . query = {
47+ code : 'foo' ,
48+ state : encodeURIComponent ( btoa ( `${ decodedUrl } _luigiNonce=SOME_NONCE` ) ) ,
49+ } as any ;
50+
5151 const authTokenResponse = {
52- id_token : idToken ,
52+ id_token : 'id' ,
5353 refresh_token : 'ref' ,
54- expires_in : '12312 ' ,
55- access_token : 'access ' ,
54+ access_token : 'acc ' ,
55+ expires_in : '111 ' ,
5656 } as AuthTokenData ;
57- getTokenForCode . mockResolvedValue ( authTokenResponse ) ;
57+ authTokenServiceMock . exchangeTokenForCode . mockResolvedValue (
58+ authTokenResponse ,
59+ ) ;
5860
59- // act
60- const tokenResponse = await controller . auth ( requestMock , responseMock ) ;
61+ const result = await controller . auth ( requestMock , responseMock ) ;
6162
62- // assert
63- expect ( callback ) . toHaveBeenCalledWith (
63+ expect ( authTokenServiceMock . exchangeTokenForCode ) . toHaveBeenCalledWith (
6464 requestMock ,
6565 responseMock ,
66- authTokenResponse ,
66+ 'foo' ,
6767 ) ;
68- expect ( getTokenForCode ) . toHaveBeenCalledWith (
68+ expect ( authCallbackMock . handleSuccess ) . toHaveBeenCalledWith (
6969 requestMock ,
7070 responseMock ,
71- 'foo' ,
71+ authTokenResponse ,
7272 ) ;
73- expect ( ( tokenResponse as AuthTokenData ) . refresh_token ) . toBeUndefined ( ) ;
73+ expect ( responseMock . redirect ) . toHaveBeenCalledWith ( decodedUrl ) ;
74+ expect ( result ) . toBe ( decodedUrl ) ;
7475 } ) ;
7576
76- it ( 'should log the error if there is a problem retrieving the token' , async ( ) => {
77- // arrange
78- const getTokenForCode = jest . spyOn (
79- authTokenServiceMock ,
80- 'exchangeTokenForCode' ,
77+ it ( 'redirects to /logout with error when token exchange fails' , async ( ) => {
78+ const origin = 'http://sub.localhost:4300' ;
79+ const stateUrl = `${ origin } /path?x=1#frag` ;
80+ requestMock . query = {
81+ code : 'foo' ,
82+ state : encodeURIComponent ( btoa ( `${ stateUrl } _luigiNonce=N` ) ) ,
83+ } as any ;
84+
85+ authTokenServiceMock . exchangeTokenForCode . mockRejectedValue (
86+ new Error ( 'boom' ) ,
8187 ) ;
82- requestMock . query = { code : 'foo' } ;
83- getTokenForCode . mockRejectedValue ( new Error ( 'error' ) ) ;
8488
85- // act
86- const response = await controller . auth ( requestMock , responseMock ) ;
89+ const result = await controller . auth ( requestMock , responseMock ) ;
8790
88- // assert
89- expect ( response ) . toBeUndefined ( ) ;
90- expect ( getTokenForCode ) . toHaveBeenCalledWith (
91+ expect ( authCallbackMock . handleFailure ) . toHaveBeenCalledWith (
9192 requestMock ,
9293 responseMock ,
93- 'foo' ,
9494 ) ;
95+ expect ( responseMock . redirect ) . toHaveBeenCalledWith (
96+ `${ origin } /logout?error=loginError` ,
97+ ) ;
98+ expect ( result ) . toBe ( `${ origin } /logout?error=loginError` ) ;
9599 } ) ;
96100
97- it ( 'should ' , ( ) => { } ) ;
101+ it ( 'redirects to /logout with error when state URL domain is not allowed' , async ( ) => {
102+ const badOrigin = 'http://malicious.example.com' ;
103+ const stateUrl = `${ badOrigin } /` ;
104+ requestMock . query = {
105+ code : 'foo' ,
106+ state : encodeURIComponent ( btoa ( `${ stateUrl } _luigiNonce=N` ) ) ,
107+ } as any ;
108+
109+ const result = await controller . auth ( requestMock , responseMock ) ;
110+
111+ expect ( authTokenServiceMock . exchangeTokenForCode ) . not . toHaveBeenCalled ( ) ;
112+ expect ( authCallbackMock . handleFailure ) . toHaveBeenCalledWith (
113+ requestMock ,
114+ responseMock ,
115+ ) ;
116+ expect ( responseMock . redirect ) . toHaveBeenCalledWith (
117+ `${ badOrigin } /logout?error=loginError` ,
118+ ) ;
119+ expect ( result ) . toBe ( `${ badOrigin } /logout?error=loginError` ) ;
120+ } ) ;
98121 } ) ;
99122
100123 describe ( 'refresh' , ( ) => {
@@ -149,7 +172,6 @@ describe('AuthController', () => {
149172
150173 it ( 'should remove the auth cookies on auth server error' , async ( ) => {
151174 // arrange
152- const logoutRedirectUrl = 'logoutRedirectUrl' ;
153175 cookiesServiceMock . getAuthCookie . mockReturnValue ( 'authCookie' ) ;
154176 authTokenServiceMock . exchangeTokenForRefreshToken . mockRejectedValue (
155177 new Error ( 'error' ) ,
0 commit comments