@@ -9327,18 +9327,18 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93279327 // Clean up environment variables after each test
93289328 delete process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] ;
93299329 delete process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] ;
9330+ delete process . env . AUTH0_DPOP_CLOCK_SKEW ;
9331+ delete process . env . AUTH0_DPOP_CLOCK_TOLERANCE ;
93309332 } ) ;
93319333
9332- it ( "should not validate dpopKeyPair during construction " , async ( ) => {
9334+ it ( "should include dpop_jkt in authorization URL when dpopKeyPair is provided " , async ( ) => {
93339335 const secret = await generateSecret ( 32 ) ;
93349336 const transactionStore = new TransactionStore ( { secret } ) ;
93359337 const sessionStore = new StatelessSessionStore ( { secret } ) ;
93369338
93379339 const { generateDpopKeyPair } = await import ( "../utils/dpopRetry.js" ) ;
93389340 const mockKeypair = await generateDpopKeyPair ( ) ;
93399341
9340- const warnSpy = vi . spyOn ( console , "warn" ) ;
9341-
93429342 const authClient = new AuthClient ( {
93439343 transactionStore,
93449344 sessionStore,
@@ -9349,16 +9349,28 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93499349 appBaseUrl : DEFAULT . appBaseUrl ,
93509350 routes : getDefaultRoutes ( ) ,
93519351 useDPoP : true ,
9352- dpopKeyPair : mockKeypair
9352+ dpopKeyPair : mockKeypair ,
9353+ fetch : getMockAuthorizationServer ( )
93539354 } ) ;
93549355
9355- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9356- expect ( warnSpy ) . not . toHaveBeenCalled ( ) ;
9356+ const request = new NextRequest (
9357+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9358+ { method : "GET" }
9359+ ) ;
93579360
9358- warnSpy . mockRestore ( ) ;
9361+ const response = await authClient . handleLogin ( request ) ;
9362+ expect ( response . status ) . toEqual ( 307 ) ;
9363+
9364+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
9365+
9366+ // Verify DPoP is enabled (dpop_jkt parameter is present)
9367+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( true ) ;
9368+ expect ( authorizationUrl . searchParams . get ( "dpop_jkt" ) ) . toMatch (
9369+ / ^ [ A - Z a - z 0 - 9 _ - ] + $ /
9370+ ) ;
93599371 } ) ;
93609372
9361- it ( "should load and validate DPoP keypair from environment variables when DPoP operations are triggered " , async ( ) => {
9373+ it ( "should load DPoP keypair from environment variables and include dpop_jkt " , async ( ) => {
93629374 process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = TEST_PRIVATE_KEY_PEM ;
93639375 process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
93649376
@@ -9379,22 +9391,27 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
93799391 fetch : getMockAuthorizationServer ( )
93809392 } ) ;
93819393
9382- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9394+ const request = new NextRequest (
9395+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9396+ { method : "GET" }
9397+ ) ;
93839398
9384- const warnSpy = vi . spyOn ( console , "warn" ) ;
9399+ const response = await authClient . handleLogin ( request ) ;
9400+ expect ( response . status ) . toEqual ( 307 ) ;
93859401
9386- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9402+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
93879403
9388- expect ( warnSpy ) . not . toHaveBeenCalledWith (
9389- expect . stringContaining ( "Failed to load DPoP keypair" )
9404+ // Verify DPoP is enabled (keypair loaded from env vars, dpop_jkt is present)
9405+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( true ) ;
9406+ expect ( authorizationUrl . searchParams . get ( "dpop_jkt" ) ) . toMatch (
9407+ / ^ [ A - Z a - z 0 - 9 _ - ] + $ /
93909408 ) ;
9391-
9392- warnSpy . mockRestore ( ) ;
93939409 } ) ;
93949410
93959411 it ( "should prioritize provided dpopKeyPair over environment variables" , async ( ) => {
9396- process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = TEST_PRIVATE_KEY_PEM ;
9397- process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
9412+ // Set INVALID env vars to ensure they are NOT used
9413+ process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = "invalid-private-key" ;
9414+ process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = "invalid-public-key" ;
93989415
93999416 const secret = await generateSecret ( 32 ) ;
94009417 const transactionStore = new TransactionStore ( { secret } ) ;
@@ -9417,18 +9434,33 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94179434 fetch : getMockAuthorizationServer ( )
94189435 } ) ;
94199436
9420- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9421- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9437+ const request = new NextRequest (
9438+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9439+ { method : "GET" }
9440+ ) ;
9441+
9442+ const response = await authClient . handleLogin ( request ) ;
9443+ expect ( response . status ) . toEqual ( 307 ) ;
9444+
9445+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
9446+
9447+ // Verify DPoP is enabled with provided keypair (not invalid env vars)
9448+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( true ) ;
9449+ expect ( authorizationUrl . searchParams . get ( "dpop_jkt" ) ) . toMatch (
9450+ / ^ [ A - Z a - z 0 - 9 _ - ] + $ /
9451+ ) ;
94229452 } ) ;
94239453
9424- it ( "should handle invalid PEM format from environment variables during lazy validation " , async ( ) => {
9454+ it ( "should fall back to bearer auth when keypair not provided and environment variables contain invalid keys " , async ( ) => {
94259455 process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = "invalid-private-key" ;
94269456 process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = "invalid-public-key" ;
94279457
94289458 const secret = await generateSecret ( 32 ) ;
94299459 const transactionStore = new TransactionStore ( { secret } ) ;
94309460 const sessionStore = new StatelessSessionStore ( { secret } ) ;
94319461
9462+ const warnSpy = vi . spyOn ( console , "warn" ) ;
9463+
94329464 const authClient = new AuthClient ( {
94339465 transactionStore,
94349466 sessionStore,
@@ -9442,11 +9474,19 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94429474 fetch : getMockAuthorizationServer ( )
94439475 } ) ;
94449476
9445- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9477+ const loginRequest = new NextRequest (
9478+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9479+ { method : "GET" }
9480+ ) ;
9481+ const loginResponse = await authClient . handleLogin ( loginRequest ) ;
9482+ expect ( loginResponse . status ) . toEqual ( 307 ) ;
94469483
9447- const warnSpy = vi . spyOn ( console , "warn" ) ;
9448- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9484+ const authorizationUrl = new URL ( loginResponse . headers . get ( "Location" ) ! ) ;
94499485
9486+ // Verify DPoP was NOT enabled (invalid keys = falls back to bearer auth)
9487+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( false ) ;
9488+
9489+ // Verify warning about failed key loading
94509490 expect ( warnSpy ) . toHaveBeenCalledWith (
94519491 expect . stringContaining (
94529492 "Failed to load DPoP keypair from environment variables"
@@ -9456,14 +9496,49 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94569496 warnSpy . mockRestore ( ) ;
94579497 } ) ;
94589498
9459- it ( "should skip DPoP validation entirely when useDPoP is false" , async ( ) => {
9499+ it ( "should not include dpop_jkt when useDPoP is false" , async ( ) => {
94609500 process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = TEST_PRIVATE_KEY_PEM ;
94619501 process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
94629502
94639503 const secret = await generateSecret ( 32 ) ;
94649504 const transactionStore = new TransactionStore ( { secret } ) ;
94659505 const sessionStore = new StatelessSessionStore ( { secret } ) ;
94669506
9507+ const authClient = new AuthClient ( {
9508+ transactionStore,
9509+ sessionStore,
9510+ domain : DEFAULT . domain ,
9511+ clientId : DEFAULT . clientId ,
9512+ clientSecret : DEFAULT . clientSecret ,
9513+ secret,
9514+ appBaseUrl : DEFAULT . appBaseUrl ,
9515+ routes : getDefaultRoutes ( ) ,
9516+ useDPoP : false ,
9517+ fetch : getMockAuthorizationServer ( )
9518+ } ) ;
9519+
9520+ const request = new NextRequest (
9521+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9522+ { method : "GET" }
9523+ ) ;
9524+
9525+ const response = await authClient . handleLogin ( request ) ;
9526+ expect ( response . status ) . toEqual ( 307 ) ;
9527+
9528+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
9529+
9530+ // Verify DPoP is NOT enabled (useDPoP=false)
9531+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( false ) ;
9532+ } ) ;
9533+
9534+ it ( "should fall back to bearer auth when only public key is in environment variables" , async ( ) => {
9535+ // Only public key set, private key missing
9536+ process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
9537+
9538+ const secret = await generateSecret ( 32 ) ;
9539+ const transactionStore = new TransactionStore ( { secret } ) ;
9540+ const sessionStore = new StatelessSessionStore ( { secret } ) ;
9541+
94679542 const warnSpy = vi . spyOn ( console , "warn" ) ;
94689543
94699544 const authClient = new AuthClient ( {
@@ -9475,29 +9550,43 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
94759550 secret,
94769551 appBaseUrl : DEFAULT . appBaseUrl ,
94779552 routes : getDefaultRoutes ( ) ,
9478- useDPoP : false ,
9553+ useDPoP : true ,
94799554 fetch : getMockAuthorizationServer ( )
94809555 } ) ;
94819556
9482- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9557+ const request = new NextRequest (
9558+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9559+ { method : "GET" }
9560+ ) ;
9561+
9562+ const response = await authClient . handleLogin ( request ) ;
9563+ expect ( response . status ) . toEqual ( 307 ) ;
94839564
9484- // Trigger operations that would validate DPoP if it were enabled
9485- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9565+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
94869566
9487- // Verify NO DPoP-related warnings were issued - validation was completely skipped
9488- expect ( warnSpy ) . not . toHaveBeenCalledWith ( expect . stringContaining ( "DPoP" ) ) ;
9489- expect ( warnSpy ) . not . toHaveBeenCalledWith ( expect . stringContaining ( "dpop" ) ) ;
9567+ // Verify DPoP was NOT enabled (missing private key)
9568+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( false ) ;
9569+
9570+ // Verify warning about missing keypair
9571+ expect ( warnSpy ) . toHaveBeenCalledWith (
9572+ expect . stringContaining (
9573+ "useDPoP is set to true but dpopKeyPair is not provided"
9574+ )
9575+ ) ;
94909576
94919577 warnSpy . mockRestore ( ) ;
94929578 } ) ;
94939579
9494- it ( "should handle missing private key only from environment variables" , async ( ) => {
9495- process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
9580+ it ( "should fall back to bearer auth when only private key is in environment variables" , async ( ) => {
9581+ // Only private key set, public key missing
9582+ process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = TEST_PRIVATE_KEY_PEM ;
94969583
94979584 const secret = await generateSecret ( 32 ) ;
94989585 const transactionStore = new TransactionStore ( { secret } ) ;
94999586 const sessionStore = new StatelessSessionStore ( { secret } ) ;
95009587
9588+ const warnSpy = vi . spyOn ( console , "warn" ) ;
9589+
95019590 const authClient = new AuthClient ( {
95029591 transactionStore,
95039592 sessionStore,
@@ -9511,12 +9600,34 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
95119600 fetch : getMockAuthorizationServer ( )
95129601 } ) ;
95139602
9514- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9515- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9603+ const request = new NextRequest (
9604+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9605+ { method : "GET" }
9606+ ) ;
9607+
9608+ const response = await authClient . handleLogin ( request ) ;
9609+ expect ( response . status ) . toEqual ( 307 ) ;
9610+
9611+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
9612+
9613+ // Verify DPoP was NOT enabled (missing public key)
9614+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( false ) ;
9615+
9616+ // Verify warning about missing keypair
9617+ expect ( warnSpy ) . toHaveBeenCalledWith (
9618+ expect . stringContaining (
9619+ "useDPoP is set to true but dpopKeyPair is not provided"
9620+ )
9621+ ) ;
9622+
9623+ warnSpy . mockRestore ( ) ;
95169624 } ) ;
95179625
9518- it ( "should handle missing public key only from environment variables" , async ( ) => {
9626+ it ( "should update clientMetadata with clockSkew and clockTolerance from environment variables" , async ( ) => {
95199627 process . env [ ENV_VARS . DPOP_PRIVATE_KEY ] = TEST_PRIVATE_KEY_PEM ;
9628+ process . env [ ENV_VARS . DPOP_PUBLIC_KEY ] = TEST_PUBLIC_KEY_PEM ;
9629+ process . env . AUTH0_DPOP_CLOCK_SKEW = "15" ;
9630+ process . env . AUTH0_DPOP_CLOCK_TOLERANCE = "30" ;
95209631
95219632 const secret = await generateSecret ( 32 ) ;
95229633 const transactionStore = new TransactionStore ( { secret } ) ;
@@ -9535,8 +9646,28 @@ ykwV8CV22wKDubrDje1vchfTL/ygX6p27RKpJm8eAH7k3EwVeg3NDfNVzQ==
95359646 fetch : getMockAuthorizationServer ( )
95369647 } ) ;
95379648
9538- expect ( authClient ) . toBeInstanceOf ( AuthClient ) ;
9539- await expect ( authClient . startInteractiveLogin ( ) ) . resolves . toBeDefined ( ) ;
9649+ // Before triggering DPoP operations, clientMetadata should not have the values
9650+ expect ( authClient [ "clientMetadata" ] [ oauth . clockSkew ] ) . toBeUndefined ( ) ;
9651+ expect (
9652+ authClient [ "clientMetadata" ] [ oauth . clockTolerance ]
9653+ ) . toBeUndefined ( ) ;
9654+
9655+ const request = new NextRequest (
9656+ new URL ( "/auth/login" , DEFAULT . appBaseUrl ) ,
9657+ { method : "GET" }
9658+ ) ;
9659+
9660+ const response = await authClient . handleLogin ( request ) ;
9661+ expect ( response . status ) . toEqual ( 307 ) ;
9662+
9663+ const authorizationUrl = new URL ( response . headers . get ( "Location" ) ! ) ;
9664+
9665+ // Verify DPoP is enabled
9666+ expect ( authorizationUrl . searchParams . has ( "dpop_jkt" ) ) . toBe ( true ) ;
9667+
9668+ // After lazy validation, clientMetadata should contain env var values
9669+ expect ( authClient [ "clientMetadata" ] [ oauth . clockSkew ] ) . toBe ( 15 ) ;
9670+ expect ( authClient [ "clientMetadata" ] [ oauth . clockTolerance ] ) . toBe ( 30 ) ;
95409671 } ) ;
95419672 } ) ;
95429673} ) ;
0 commit comments