@@ -760,6 +760,15 @@ export class AuthClient {
760760
761761 try {
762762 redirectUri = createRouteUrl ( this . routes . callback , this . appBaseUrl ) ; // must be registered with the authorization server
763+
764+ // Create DPoP handle ONCE outside the closure so it persists across retries.
765+ // This is required by RFC 9449: the handle must learn and reuse the nonce from
766+ // the DPoP-Nonce header across multiple attempts.
767+ const dpopHandle =
768+ this . useDPoP && this . dpopKeyPair
769+ ? oauth . DPoP ( this . clientMetadata , this . dpopKeyPair )
770+ : undefined ;
771+
763772 authorizationCodeGrantRequestCall = async ( ) =>
764773 oauth . authorizationCodeGrantRequest (
765774 authorizationServerMetadata ,
@@ -772,14 +781,25 @@ export class AuthClient {
772781 ...this . httpOptions ( ) ,
773782 [ oauth . customFetch ] : this . fetch ,
774783 [ oauth . allowInsecureRequests ] : this . allowInsecureRequests ,
775- ...( this . useDPoP &&
776- this . dpopKeyPair && {
777- DPoP : oauth . DPoP ( this . clientMetadata , this . dpopKeyPair ! )
778- } )
784+ ...( dpopHandle && {
785+ DPoP : dpopHandle
786+ } )
779787 }
780788 ) ;
781789
782- codeGrantResponse = await authorizationCodeGrantRequestCall ( ) ;
790+ // NOTE: Unlike refresh token and connection token flows, the auth code flow
791+ // wraps only the HTTP request (not response processing) in withDPoPNonceRetry().
792+ // This is intentional: withDPoPNonceRetry() expects a Response object to inspect
793+ // for nonce retries. If response processing is included in the wrapper, it returns
794+ // a processed object and retry logic breaks. Response processing happens after
795+ // (see line 807) to maintain compatibility with the retry mechanism.
796+ codeGrantResponse = await withDPoPNonceRetry (
797+ authorizationCodeGrantRequestCall ,
798+ {
799+ isDPoPEnabled : ! ! ( this . useDPoP && this . dpopKeyPair ) ,
800+ ...this . dpopOptions ?. retry
801+ }
802+ ) ;
783803 } catch ( e : any ) {
784804 return this . handleCallbackError (
785805 new AuthorizationCodeGrantRequestError ( e . message ) ,
@@ -1202,6 +1222,14 @@ export class AuthClient {
12021222 additionalParameters . append ( "audience" , options . audience ) ;
12031223 }
12041224
1225+ // Create DPoP handle ONCE outside the closure so it persists across retries.
1226+ // This is required by RFC 9449: the handle must learn and reuse the nonce from
1227+ // the DPoP-Nonce header across multiple attempts.
1228+ const dpopHandle =
1229+ this . useDPoP && this . dpopKeyPair
1230+ ? oauth . DPoP ( this . clientMetadata , this . dpopKeyPair )
1231+ : undefined ;
1232+
12051233 const refreshTokenGrantRequestCall = async ( ) =>
12061234 oauth . refreshTokenGrantRequest (
12071235 authorizationServerMetadata ,
@@ -1213,10 +1241,9 @@ export class AuthClient {
12131241 [ oauth . customFetch ] : this . fetch ,
12141242 [ oauth . allowInsecureRequests ] : this . allowInsecureRequests ,
12151243 additionalParameters,
1216- ...( this . useDPoP &&
1217- this . dpopKeyPair && {
1218- DPoP : oauth . DPoP ( this . clientMetadata , this . dpopKeyPair ! )
1219- } )
1244+ ...( dpopHandle && {
1245+ DPoP : dpopHandle
1246+ } )
12201247 }
12211248 ) ;
12221249
@@ -1229,10 +1256,16 @@ export class AuthClient {
12291256
12301257 let oauthRes : oauth . TokenEndpointResponse ;
12311258 try {
1232- oauthRes = await withDPoPNonceRetry ( async ( ) => {
1233- const refreshTokenRes = await refreshTokenGrantRequestCall ( ) ;
1234- return await processRefreshTokenResponseCall ( refreshTokenRes ) ;
1235- } , this . dpopOptions ?. retry ) ;
1259+ oauthRes = await withDPoPNonceRetry (
1260+ async ( ) => {
1261+ const refreshTokenRes = await refreshTokenGrantRequestCall ( ) ;
1262+ return await processRefreshTokenResponseCall ( refreshTokenRes ) ;
1263+ } ,
1264+ {
1265+ isDPoPEnabled : ! ! ( this . useDPoP && this . dpopKeyPair ) ,
1266+ ...this . dpopOptions ?. retry
1267+ }
1268+ ) ;
12361269 } catch ( e : any ) {
12371270 return [
12381271 new AccessTokenError (
@@ -1745,6 +1778,14 @@ export class AuthClient {
17451778 return [ discoveryError , null ] ;
17461779 }
17471780
1781+ // Create DPoP handle ONCE outside the closure so it persists across retries.
1782+ // This is required by RFC 9449: the handle must learn and reuse the nonce from
1783+ // the DPoP-Nonce header across multiple attempts.
1784+ const dpopHandle =
1785+ this . useDPoP && this . dpopKeyPair
1786+ ? oauth . DPoP ( this . clientMetadata , this . dpopKeyPair )
1787+ : undefined ;
1788+
17481789 const genericTokenEndpointRequestCall = async ( ) =>
17491790 oauth . genericTokenEndpointRequest (
17501791 authorizationServerMetadata ,
@@ -1755,26 +1796,30 @@ export class AuthClient {
17551796 {
17561797 [ oauth . customFetch ] : this . fetch ,
17571798 [ oauth . allowInsecureRequests ] : this . allowInsecureRequests ,
1758- ...( this . useDPoP &&
1759- this . dpopKeyPair && {
1760- DPoP : oauth . DPoP ( this . clientMetadata , this . dpopKeyPair ! )
1761- } )
1799+ ...( dpopHandle && {
1800+ DPoP : dpopHandle
1801+ } )
17621802 }
17631803 ) ;
17641804
1765- const processGenericTokenEndpointResponseCall = ( response : Response ) =>
1766- oauth . processGenericTokenEndpointResponse (
1805+ const processGenericTokenEndpointResponseCall = async ( ) => {
1806+ const httpResponse = await genericTokenEndpointRequestCall ( ) ;
1807+ return oauth . processGenericTokenEndpointResponse (
17671808 authorizationServerMetadata ,
17681809 this . clientMetadata ,
1769- response
1810+ httpResponse
17701811 ) ;
1812+ } ;
17711813
17721814 let tokenEndpointResponse : oauth . TokenEndpointResponse ;
17731815 try {
1774- tokenEndpointResponse = await withDPoPNonceRetry ( async ( ) => {
1775- const httpResponse = await genericTokenEndpointRequestCall ( ) ;
1776- return await processGenericTokenEndpointResponseCall ( httpResponse ) ;
1777- } , this . dpopOptions ?. retry ) ;
1816+ tokenEndpointResponse = await withDPoPNonceRetry (
1817+ processGenericTokenEndpointResponseCall ,
1818+ {
1819+ isDPoPEnabled : ! ! ( this . useDPoP && this . dpopKeyPair ) ,
1820+ ...this . dpopOptions ?. retry
1821+ }
1822+ ) ;
17781823 } catch ( err : any ) {
17791824 return [
17801825 new AccessTokenForConnectionError (
0 commit comments