Skip to content

Commit 751e55d

Browse files
fix: make sure retry happens on nonce error for authcode flow; use common dpop handler across authcode, gettokenset, getconnnectiontokenset; refactor withDPoPNonceRetry
1 parent 958259f commit 751e55d

File tree

3 files changed

+404
-52
lines changed

3 files changed

+404
-52
lines changed

src/server/auth-client.ts

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ export class AuthClient {
246246

247247
private dpopKeyPair?: DpopKeyPair;
248248
private readonly useDPoP: boolean;
249+
private defaultDPoPHandle?: ReturnType<typeof oauth.DPoP>;
249250

250251
constructor(options: AuthClientOptions) {
251252
// dependencies
@@ -366,6 +367,12 @@ export class AuthClient {
366367
// Initialize DPoP if enabled. Check useDPoP flag first to avoid timing attacks.
367368
if ((options.useDPoP ?? false) && options.dpopKeyPair) {
368369
this.dpopKeyPair = options.dpopKeyPair;
370+
// Create DPoP handle once for reuse across all token grant operations
371+
// oauth4webapi automatically learns nonces from error responses
372+
this.defaultDPoPHandle = oauth.DPoP(
373+
this.clientMetadata,
374+
options.dpopKeyPair
375+
);
369376
}
370377
}
371378

@@ -772,14 +779,17 @@ export class AuthClient {
772779
...this.httpOptions(),
773780
[oauth.customFetch]: this.fetch,
774781
[oauth.allowInsecureRequests]: this.allowInsecureRequests,
775-
...(this.useDPoP &&
776-
this.dpopKeyPair && {
777-
DPoP: oauth.DPoP(this.clientMetadata, this.dpopKeyPair!)
778-
})
782+
...(this.defaultDPoPHandle && { DPoP: this.defaultDPoPHandle })
779783
}
780784
);
781785

782-
codeGrantResponse = await authorizationCodeGrantRequestCall();
786+
codeGrantResponse = await withDPoPNonceRetry(
787+
authorizationCodeGrantRequestCall,
788+
{
789+
isDPoPEnabled: !!(this.useDPoP && this.dpopKeyPair),
790+
...this.dpopOptions?.retry
791+
}
792+
);
783793
} catch (e: any) {
784794
return this.handleCallbackError(
785795
new AuthorizationCodeGrantRequestError(e.message),
@@ -791,8 +801,9 @@ export class AuthClient {
791801
let oidcRes: oauth.TokenEndpointResponse;
792802
try {
793803
// Process the authorization code response
794-
// For authorization code flows, oauth4webapi handles DPoP nonce management internally
795-
// No need for manual retry since authorization codes are single-use
804+
// When DPoP is enabled, the nonce retry logic is handled above in the
805+
// authorizationCodeGrantRequestCall, so codeGrantResponse is guaranteed
806+
// to have been obtained with proper nonce handling if needed
796807
oidcRes = await oauth.processAuthorizationCodeResponse(
797808
authorizationServerMetadata,
798809
this.clientMetadata,
@@ -1213,10 +1224,7 @@ export class AuthClient {
12131224
[oauth.customFetch]: this.fetch,
12141225
[oauth.allowInsecureRequests]: this.allowInsecureRequests,
12151226
additionalParameters,
1216-
...(this.useDPoP &&
1217-
this.dpopKeyPair && {
1218-
DPoP: oauth.DPoP(this.clientMetadata, this.dpopKeyPair!)
1219-
})
1227+
...(this.defaultDPoPHandle && { DPoP: this.defaultDPoPHandle })
12201228
}
12211229
);
12221230

@@ -1229,10 +1237,16 @@ export class AuthClient {
12291237

12301238
let oauthRes: oauth.TokenEndpointResponse;
12311239
try {
1232-
oauthRes = await withDPoPNonceRetry(async () => {
1233-
const refreshTokenRes = await refreshTokenGrantRequestCall();
1234-
return await processRefreshTokenResponseCall(refreshTokenRes);
1235-
}, this.dpopOptions?.retry);
1240+
oauthRes = await withDPoPNonceRetry(
1241+
async () => {
1242+
const refreshTokenRes = await refreshTokenGrantRequestCall();
1243+
return await processRefreshTokenResponseCall(refreshTokenRes);
1244+
},
1245+
{
1246+
isDPoPEnabled: !!(this.useDPoP && this.dpopKeyPair),
1247+
...this.dpopOptions?.retry
1248+
}
1249+
);
12361250
} catch (e: any) {
12371251
return [
12381252
new AccessTokenError(
@@ -1755,26 +1769,28 @@ export class AuthClient {
17551769
{
17561770
[oauth.customFetch]: this.fetch,
17571771
[oauth.allowInsecureRequests]: this.allowInsecureRequests,
1758-
...(this.useDPoP &&
1759-
this.dpopKeyPair && {
1760-
DPoP: oauth.DPoP(this.clientMetadata, this.dpopKeyPair!)
1761-
})
1772+
...(this.defaultDPoPHandle && { DPoP: this.defaultDPoPHandle })
17621773
}
17631774
);
17641775

1765-
const processGenericTokenEndpointResponseCall = (response: Response) =>
1766-
oauth.processGenericTokenEndpointResponse(
1776+
const processGenericTokenEndpointResponseCall = async () => {
1777+
const httpResponse = await genericTokenEndpointRequestCall();
1778+
return oauth.processGenericTokenEndpointResponse(
17671779
authorizationServerMetadata,
17681780
this.clientMetadata,
1769-
response
1781+
httpResponse
17701782
);
1783+
};
17711784

17721785
let tokenEndpointResponse: oauth.TokenEndpointResponse;
17731786
try {
1774-
tokenEndpointResponse = await withDPoPNonceRetry(async () => {
1775-
const httpResponse = await genericTokenEndpointRequestCall();
1776-
return await processGenericTokenEndpointResponseCall(httpResponse);
1777-
}, this.dpopOptions?.retry);
1787+
tokenEndpointResponse = await withDPoPNonceRetry(
1788+
processGenericTokenEndpointResponseCall,
1789+
{
1790+
isDPoPEnabled: !!(this.useDPoP && this.dpopKeyPair),
1791+
...this.dpopOptions?.retry
1792+
}
1793+
);
17781794
} catch (err: any) {
17791795
return [
17801796
new AccessTokenForConnectionError(

0 commit comments

Comments
 (0)