Skip to content

Commit 3009c49

Browse files
fix: make sure session is updated with latest accesstoken before calling finalizeSession
1 parent 958259f commit 3009c49

File tree

3 files changed

+478
-55
lines changed

3 files changed

+478
-55
lines changed

src/server/auth-client.ts

Lines changed: 69 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)