@@ -4214,6 +4214,250 @@ ca/T0LLtgmbMmxSv/MmzIg==
42144214 } ) ;
42154215 } ) ;
42164216 } ) ;
4217+
4218+ describe ( "startInteractiveLogin" , async ( ) => {
4219+ const createAuthClient = async ( {
4220+ pushedAuthorizationRequests = false ,
4221+ signInReturnToPath = "/" ,
4222+ authorizationParameters = { }
4223+ } = { } ) => {
4224+ const secret = await generateSecret ( 32 ) ;
4225+ const transactionStore = new TransactionStore ( {
4226+ secret
4227+ } ) ;
4228+ const sessionStore = new StatelessSessionStore ( {
4229+ secret
4230+ } ) ;
4231+
4232+ return new AuthClient ( {
4233+ transactionStore,
4234+ sessionStore,
4235+
4236+ domain : DEFAULT . domain ,
4237+ clientId : DEFAULT . clientId ,
4238+ clientSecret : DEFAULT . clientSecret ,
4239+
4240+ secret,
4241+ appBaseUrl : DEFAULT . appBaseUrl ,
4242+ signInReturnToPath,
4243+ pushedAuthorizationRequests,
4244+ authorizationParameters : {
4245+ scope : "openid profile email" ,
4246+ ...authorizationParameters
4247+ } ,
4248+
4249+ fetch : getMockAuthorizationServer ( )
4250+ } ) ;
4251+ } ;
4252+
4253+ it ( "should use the default returnTo path when no returnTo is provided" , async ( ) => {
4254+ const defaultReturnTo = "/default-path" ;
4255+ const authClient = await createAuthClient ( {
4256+ signInReturnToPath : defaultReturnTo
4257+ } ) ;
4258+
4259+ // Mock the transactionStore.save method to verify the saved state
4260+ const originalSave = authClient [ 'transactionStore' ] . save ;
4261+ authClient [ 'transactionStore' ] . save = vi . fn ( async ( cookies , state ) => {
4262+ expect ( state . returnTo ) . toBe ( defaultReturnTo ) ;
4263+ return originalSave . call ( authClient [ 'transactionStore' ] , cookies , state ) ;
4264+ } ) ;
4265+
4266+ await authClient . startInteractiveLogin ( ) ;
4267+
4268+ expect ( authClient [ 'transactionStore' ] . save ) . toHaveBeenCalled ( ) ;
4269+ } ) ;
4270+
4271+ it ( "should sanitize and use the provided returnTo parameter" , async ( ) => {
4272+ const authClient = await createAuthClient ( ) ;
4273+ const returnTo = "/custom-return-path" ;
4274+
4275+ // Mock the transactionStore.save method to verify the saved state
4276+ const originalSave = authClient [ 'transactionStore' ] . save ;
4277+ authClient [ 'transactionStore' ] . save = vi . fn ( async ( cookies , state ) => {
4278+ // The full URL is saved, not just the path
4279+ expect ( state . returnTo ) . toBe ( "https://example.com/custom-return-path" ) ;
4280+ return originalSave . call ( authClient [ 'transactionStore' ] , cookies , state ) ;
4281+ } ) ;
4282+
4283+ await authClient . startInteractiveLogin ( { returnTo } ) ;
4284+
4285+ expect ( authClient [ 'transactionStore' ] . save ) . toHaveBeenCalled ( ) ;
4286+ } ) ;
4287+
4288+ it ( "should reject unsafe returnTo URLs" , async ( ) => {
4289+ const authClient = await createAuthClient ( {
4290+ signInReturnToPath : "/safe-path"
4291+ } ) ;
4292+ const unsafeReturnTo = "https://malicious-site.com" ;
4293+
4294+ // Mock the transactionStore.save method to verify the saved state
4295+ const originalSave = authClient [ 'transactionStore' ] . save ;
4296+ authClient [ 'transactionStore' ] . save = vi . fn ( async ( cookies , state ) => {
4297+ // Should use the default safe path instead of the malicious one
4298+ expect ( state . returnTo ) . toBe ( "/safe-path" ) ;
4299+ return originalSave . call ( authClient [ 'transactionStore' ] , cookies , state ) ;
4300+ } ) ;
4301+
4302+ await authClient . startInteractiveLogin ( { returnTo : unsafeReturnTo } ) ;
4303+
4304+ expect ( authClient [ 'transactionStore' ] . save ) . toHaveBeenCalled ( ) ;
4305+ } ) ;
4306+
4307+ it ( "should pass authorization parameters to the authorization URL" , async ( ) => {
4308+ const authClient = await createAuthClient ( ) ;
4309+ const authorizationParameters = {
4310+ audience : "https://api.example.com" ,
4311+ scope : "openid profile email custom_scope"
4312+ } ;
4313+
4314+ // Spy on the authorizationUrl method to verify the passed params
4315+ const originalAuthorizationUrl = authClient [ 'authorizationUrl' ] ;
4316+ authClient [ 'authorizationUrl' ] = vi . fn ( async ( params ) => {
4317+ // Verify the audience is set correctly
4318+ expect ( params . get ( "audience" ) ) . toBe ( authorizationParameters . audience ) ;
4319+ // Verify the scope is set correctly
4320+ expect ( params . get ( "scope" ) ) . toBe ( authorizationParameters . scope ) ;
4321+ return originalAuthorizationUrl . call ( authClient , params ) ;
4322+ } ) ;
4323+
4324+ await authClient . startInteractiveLogin ( { authorizationParameters } ) ;
4325+
4326+ expect ( authClient [ 'authorizationUrl' ] ) . toHaveBeenCalled ( ) ;
4327+ } ) ;
4328+
4329+ it ( "should handle pushed authorization requests (PAR) correctly" , async ( ) => {
4330+ let parRequestCalled = false ;
4331+ const mockFetch = getMockAuthorizationServer ( {
4332+ onParRequest : async ( ) => {
4333+ parRequestCalled = true ;
4334+ }
4335+ } ) ;
4336+
4337+ const secret = await generateSecret ( 32 ) ;
4338+ const transactionStore = new TransactionStore ( { secret } ) ;
4339+ const sessionStore = new StatelessSessionStore ( { secret } ) ;
4340+
4341+ const authClient = new AuthClient ( {
4342+ transactionStore,
4343+ sessionStore,
4344+ domain : DEFAULT . domain ,
4345+ clientId : DEFAULT . clientId ,
4346+ clientSecret : DEFAULT . clientSecret ,
4347+ secret,
4348+ appBaseUrl : DEFAULT . appBaseUrl ,
4349+ pushedAuthorizationRequests : true ,
4350+ authorizationParameters : {
4351+ scope : "openid profile email"
4352+ } ,
4353+ fetch : mockFetch
4354+ } ) ;
4355+
4356+ await authClient . startInteractiveLogin ( ) ;
4357+
4358+ // Verify that PAR was used
4359+ expect ( parRequestCalled ) . toBe ( true ) ;
4360+ } ) ;
4361+
4362+ it ( "should save the transaction state with correct values" , async ( ) => {
4363+ const authClient = await createAuthClient ( ) ;
4364+ const returnTo = "/custom-path" ;
4365+
4366+ // Instead of mocking the oauth functions, we'll just check the structure of the transaction state
4367+ const originalSave = authClient [ 'transactionStore' ] . save ;
4368+ authClient [ 'transactionStore' ] . save = vi . fn ( async ( cookies , transactionState ) => {
4369+ expect ( transactionState ) . toEqual ( expect . objectContaining ( {
4370+ nonce : expect . any ( String ) ,
4371+ codeVerifier : expect . any ( String ) ,
4372+ responseType : "code" ,
4373+ state : expect . any ( String ) ,
4374+ returnTo : "https://example.com/custom-path"
4375+ } ) ) ;
4376+ return originalSave . call ( authClient [ 'transactionStore' ] , cookies , transactionState ) ;
4377+ } ) ;
4378+
4379+ await authClient . startInteractiveLogin ( { returnTo } ) ;
4380+
4381+ expect ( authClient [ 'transactionStore' ] . save ) . toHaveBeenCalled ( ) ;
4382+ } ) ;
4383+
4384+ it ( "should merge configuration authorizationParameters with method arguments" , async ( ) => {
4385+ const configScope = "openid profile email" ;
4386+ const configAudience = "https://default-api.example.com" ;
4387+ const authClient = await createAuthClient ( {
4388+ authorizationParameters : {
4389+ scope : configScope ,
4390+ audience : configAudience
4391+ }
4392+ } ) ;
4393+
4394+ const methodScope = "openid profile email custom_scope" ;
4395+ const methodAudience = "https://custom-api.example.com" ;
4396+
4397+ // Spy on the authorizationUrl method to verify the passed params
4398+ const originalAuthorizationUrl = authClient [ 'authorizationUrl' ] ;
4399+ authClient [ 'authorizationUrl' ] = vi . fn ( async ( params ) => {
4400+ // Method's authorization parameters should override config
4401+ expect ( params . get ( "audience" ) ) . toBe ( methodAudience ) ;
4402+ expect ( params . get ( "scope" ) ) . toBe ( methodScope ) ;
4403+ return originalAuthorizationUrl . call ( authClient , params ) ;
4404+ } ) ;
4405+
4406+ await authClient . startInteractiveLogin ( {
4407+ authorizationParameters : {
4408+ scope : methodScope ,
4409+ audience : methodAudience
4410+ }
4411+ } ) ;
4412+
4413+ expect ( authClient [ 'authorizationUrl' ] ) . toHaveBeenCalled ( ) ;
4414+ } ) ;
4415+
4416+ // Add tests for handleLogin method
4417+ it ( "should create correct options in handleLogin with returnTo parameter" , async ( ) => {
4418+ const authClient = await createAuthClient ( ) ;
4419+
4420+ // Mock startInteractiveLogin to check what options are passed to it
4421+ const originalStartInteractiveLogin = authClient . startInteractiveLogin ;
4422+ authClient . startInteractiveLogin = vi . fn ( async ( options ) => {
4423+ expect ( options ) . toEqual ( {
4424+ authorizationParameters : { foo : "bar" , returnTo : "custom-return" } ,
4425+ returnTo : "custom-return"
4426+ } ) ;
4427+ return originalStartInteractiveLogin . call ( authClient , options ) ;
4428+ } ) ;
4429+
4430+ const reqUrl = new URL ( "https://example.com/auth/login?foo=bar&returnTo=custom-return" ) ;
4431+ const req = new NextRequest ( reqUrl , { method : "GET" } ) ;
4432+
4433+ await authClient . handleLogin ( req ) ;
4434+
4435+ expect ( authClient . startInteractiveLogin ) . toHaveBeenCalled ( ) ;
4436+ } ) ;
4437+
4438+ it ( "should handle PAR correctly in handleLogin by not forwarding params" , async ( ) => {
4439+ const authClient = await createAuthClient ( {
4440+ pushedAuthorizationRequests : true
4441+ } ) ;
4442+
4443+ // Mock startInteractiveLogin to check what options are passed to it
4444+ const originalStartInteractiveLogin = authClient . startInteractiveLogin ;
4445+ authClient . startInteractiveLogin = vi . fn ( async ( options ) => {
4446+ expect ( options ) . toEqual ( {
4447+ authorizationParameters : { } ,
4448+ returnTo : "custom-return"
4449+ } ) ;
4450+ return originalStartInteractiveLogin . call ( authClient , options ) ;
4451+ } ) ;
4452+
4453+ const reqUrl = new URL ( "https://example.com/auth/login?foo=bar&returnTo=custom-return" ) ;
4454+ const req = new NextRequest ( reqUrl , { method : "GET" } ) ;
4455+
4456+ await authClient . handleLogin ( req ) ;
4457+
4458+ expect ( authClient . startInteractiveLogin ) . toHaveBeenCalled ( ) ;
4459+ } ) ;
4460+ } ) ;
42174461} ) ;
42184462
42194463const _authorizationServerMetadata = {
0 commit comments