@@ -1169,122 +1169,21 @@ export class AuthClient {
11691169 ! tokenSet . expiresAt ||
11701170 tokenSet . expiresAt <= Date . now ( ) / 1000
11711171 ) {
1172- const [ discoveryError , authorizationServerMetadata ] =
1173- await this . discoverAuthorizationServerMetadata ( ) ;
1174-
1175- if ( discoveryError ) {
1176- return [ discoveryError , null ] ;
1177- }
1178-
1179- const additionalParameters = new URLSearchParams ( ) ;
1180-
1181- if ( options . scope ) {
1182- additionalParameters . append ( "scope" , scope ) ;
1183- }
1184-
1185- if ( options . audience ) {
1186- additionalParameters . append ( "audience" , options . audience ) ;
1187- }
1188-
1189- // Create DPoP handle ONCE outside the closure so it persists across retries.
1190- // This is required by RFC 9449: the handle must learn and reuse the nonce from
1191- // the DPoP-Nonce header across multiple attempts.
1192- const dpopHandle =
1193- this . useDPoP && this . dpopKeyPair
1194- ? oauth . DPoP ( this . clientMetadata , this . dpopKeyPair )
1195- : undefined ;
1196-
1197- const refreshTokenGrantRequestCall = async ( ) =>
1198- oauth . refreshTokenGrantRequest (
1199- authorizationServerMetadata ,
1200- this . clientMetadata ,
1201- await this . getClientAuth ( ) ,
1202- tokenSet . refreshToken ! ,
1203- {
1204- ...this . httpOptions ( ) ,
1205- [ oauth . customFetch ] : this . fetch ,
1206- [ oauth . allowInsecureRequests ] : this . allowInsecureRequests ,
1207- additionalParameters,
1208- ...( dpopHandle && {
1209- DPoP : dpopHandle
1210- } )
1211- }
1212- ) ;
1213-
1214- const processRefreshTokenResponseCall = ( response : Response ) =>
1215- oauth . processRefreshTokenResponse (
1216- authorizationServerMetadata ,
1217- this . clientMetadata ,
1218- response
1219- ) ;
1220-
1221- let oauthRes : oauth . TokenEndpointResponse ;
1222- try {
1223- oauthRes = await withDPoPNonceRetry (
1224- async ( ) => {
1225- const refreshTokenRes = await refreshTokenGrantRequestCall ( ) ;
1226- return await processRefreshTokenResponseCall ( refreshTokenRes ) ;
1227- } ,
1228- {
1229- isDPoPEnabled : ! ! ( this . useDPoP && this . dpopKeyPair ) ,
1230- ...this . dpopOptions ?. retry
1231- }
1232- ) ;
1233- } catch ( e : any ) {
1234- return [
1235- new AccessTokenError (
1236- AccessTokenErrorCode . FAILED_TO_REFRESH_TOKEN ,
1237- "The access token has expired and there was an error while trying to refresh it." ,
1238- new OAuth2Error ( {
1239- code : e . error ,
1240- message : e . error_description
1241- } )
1242- ) ,
1243- null
1244- ] ;
1245- }
1172+ const [ error , response ] = await this . #refreshTokenSet( tokenSet , {
1173+ audience : options . audience ,
1174+ scope : options . scope ? scope : undefined ,
1175+ requestedScope : scope
1176+ } ) ;
12461177
1247- const idTokenClaims = oauth . getValidatedIdTokenClaims ( oauthRes ) ! ;
1248- const accessTokenExpiresAt =
1249- Math . floor ( Date . now ( ) / 1000 ) + Number ( oauthRes . expires_in ) ;
1250-
1251- const updatedTokenSet = {
1252- ...tokenSet , // contains the existing `iat` claim to maintain the session lifetime
1253- accessToken : oauthRes . access_token ,
1254- idToken : oauthRes . id_token ,
1255- // We store the both requested and granted scopes on the tokenSet, so we know what scopes were requested.
1256- // The server may return less scopes than requested.
1257- // This ensures we can return the same token again when a token for the same or less scopes is requested by using `requestedScope` during look-up.
1258- //
1259- // E.g. When requesting a token with scope `a b`, and we return one for scope `a` only,
1260- // - If we only store the returned scopes, we cannot return this token when the user requests a token for scope `a b` again.
1261- // - If we only store the requested scopes, we lose track of the actual scopes granted.
1262- //
1263- // Scopes actually granted by the server
1264- scope : oauthRes . scope ,
1265- // Scopes requested by the client
1266- requestedScope : scope ,
1267- expiresAt : accessTokenExpiresAt ,
1268- // Keep the audience if it exists, otherwise use the one from the options.
1269- // If not provided, use `undefined`.
1270- audience : tokenSet . audience || options . audience || undefined ,
1271- // Store the token type from the OAuth response (e.g., "Bearer", "DPoP")
1272- ...( oauthRes . token_type && { token_type : oauthRes . token_type } )
1273- } ;
1274-
1275- if ( oauthRes . refresh_token ) {
1276- // refresh token rotation is enabled, persist the new refresh token from the response
1277- updatedTokenSet . refreshToken = oauthRes . refresh_token ;
1278- } else {
1279- // we did not get a refresh token back, keep the current long-lived refresh token around
1280- updatedTokenSet . refreshToken = tokenSet . refreshToken ;
1178+ if ( error ) {
1179+ return [ error , null ] ;
12811180 }
12821181
12831182 return [
12841183 null ,
12851184 {
1286- tokenSet : updatedTokenSet ,
1287- idTokenClaims : idTokenClaims
1185+ tokenSet : response . updatedTokenSet ,
1186+ idTokenClaims : response . idTokenClaims
12881187 }
12891188 ] ;
12901189 }
@@ -2241,6 +2140,145 @@ export class AuthClient {
22412140 addCacheControlHeadersForSession ( res ) ;
22422141 }
22432142 }
2143+
2144+ /**
2145+ * Refreshes the token set using the provided refresh token.
2146+ * @param tokenSet The current token set containing the refresh token.
2147+ * @param options Options for the refresh operation, including scope and audience.
2148+ * @returns A tuple containing either:
2149+ * - `[null, { updatedTokenSet: TokenSet; idTokenClaims: oauth.IDToken }]` if the token was successfully refreshed, containing the updated token set and ID token claims.
2150+ * - `[SdkError, null]` if an error occurred during the refresh process.
2151+ */
2152+ async #refreshTokenSet(
2153+ tokenSet : Partial < TokenSet > ,
2154+ options : {
2155+ scope ?: string ;
2156+ audience ?: string | null ;
2157+ requestedScope : string ;
2158+ }
2159+ ) : Promise <
2160+ | [ null , { updatedTokenSet : TokenSet ; idTokenClaims : oauth . IDToken } ]
2161+ | [ SdkError , null ]
2162+ > {
2163+ const [ discoveryError , authorizationServerMetadata ] =
2164+ await this . discoverAuthorizationServerMetadata ( ) ;
2165+
2166+ if ( discoveryError ) {
2167+ return [ discoveryError , null ] ;
2168+ }
2169+
2170+ const additionalParameters = new URLSearchParams ( ) ;
2171+
2172+ if ( options . scope ) {
2173+ additionalParameters . append ( "scope" , options . scope ) ;
2174+ }
2175+
2176+ if ( options . audience ) {
2177+ additionalParameters . append ( "audience" , options . audience ) ;
2178+ }
2179+
2180+ // Create DPoP handle ONCE outside the closure so it persists across retries.
2181+ // This is required by RFC 9449: the handle must learn and reuse the nonce from
2182+ // the DPoP-Nonce header across multiple attempts.
2183+ const dpopHandle =
2184+ this . useDPoP && this . dpopKeyPair
2185+ ? oauth . DPoP ( this . clientMetadata , this . dpopKeyPair )
2186+ : undefined ;
2187+
2188+ const refreshTokenGrantRequestCall = async ( ) =>
2189+ oauth . refreshTokenGrantRequest (
2190+ authorizationServerMetadata ,
2191+ this . clientMetadata ,
2192+ await this . getClientAuth ( ) ,
2193+ tokenSet . refreshToken ! ,
2194+ {
2195+ ...this . httpOptions ( ) ,
2196+ [ oauth . customFetch ] : this . fetch ,
2197+ [ oauth . allowInsecureRequests ] : this . allowInsecureRequests ,
2198+ additionalParameters,
2199+ ...( dpopHandle && {
2200+ DPoP : dpopHandle
2201+ } )
2202+ }
2203+ ) ;
2204+
2205+ const processRefreshTokenResponseCall = ( response : Response ) =>
2206+ oauth . processRefreshTokenResponse (
2207+ authorizationServerMetadata ,
2208+ this . clientMetadata ,
2209+ response
2210+ ) ;
2211+
2212+ let oauthRes : oauth . TokenEndpointResponse ;
2213+ try {
2214+ oauthRes = await withDPoPNonceRetry (
2215+ async ( ) => {
2216+ const refreshTokenRes = await refreshTokenGrantRequestCall ( ) ;
2217+ return await processRefreshTokenResponseCall ( refreshTokenRes ) ;
2218+ } ,
2219+ {
2220+ isDPoPEnabled : ! ! ( this . useDPoP && this . dpopKeyPair ) ,
2221+ ...this . dpopOptions ?. retry
2222+ }
2223+ ) ;
2224+ } catch ( e : any ) {
2225+ return [
2226+ new AccessTokenError (
2227+ AccessTokenErrorCode . FAILED_TO_REFRESH_TOKEN ,
2228+ "The access token has expired and there was an error while trying to refresh it." ,
2229+ new OAuth2Error ( {
2230+ code : e . error ,
2231+ message : e . error_description
2232+ } )
2233+ ) ,
2234+ null
2235+ ] ;
2236+ }
2237+
2238+ const idTokenClaims = oauth . getValidatedIdTokenClaims ( oauthRes ) ! ;
2239+ const accessTokenExpiresAt =
2240+ Math . floor ( Date . now ( ) / 1000 ) + Number ( oauthRes . expires_in ) ;
2241+
2242+ const updatedTokenSet = {
2243+ ...tokenSet , // contains the existing `iat` claim to maintain the session lifetime
2244+ accessToken : oauthRes . access_token ,
2245+ idToken : oauthRes . id_token ,
2246+ // We store the both requested and granted scopes on the tokenSet, so we know what scopes were requested.
2247+ // The server may return less scopes than requested.
2248+ // This ensures we can return the same token again when a token for the same or less scopes is requested by using `requestedScope` during look-up.
2249+ //
2250+ // E.g. When requesting a token with scope `a b`, and we return one for scope `a` only,
2251+ // - If we only store the returned scopes, we cannot return this token when the user requests a token for scope `a b` again.
2252+ // - If we only store the requested scopes, we lose track of the actual scopes granted.
2253+ //
2254+ // Scopes actually granted by the server
2255+ scope : oauthRes . scope ,
2256+ // Scopes requested by the client
2257+ requestedScope : options . requestedScope ,
2258+ expiresAt : accessTokenExpiresAt ,
2259+ // Keep the audience if it exists, otherwise use the one from the options.
2260+ // If not provided, use `undefined`.
2261+ audience : tokenSet . audience || options . audience || undefined ,
2262+ // Store the token type from the OAuth response (e.g., "Bearer", "DPoP")
2263+ ...( oauthRes . token_type && { token_type : oauthRes . token_type } )
2264+ } ;
2265+
2266+ if ( oauthRes . refresh_token ) {
2267+ // refresh token rotation is enabled, persist the new refresh token from the response
2268+ updatedTokenSet . refreshToken = oauthRes . refresh_token ;
2269+ } else {
2270+ // we did not get a refresh token back, keep the current long-lived refresh token around
2271+ updatedTokenSet . refreshToken = tokenSet . refreshToken ;
2272+ }
2273+
2274+ return [
2275+ null ,
2276+ {
2277+ updatedTokenSet,
2278+ idTokenClaims
2279+ }
2280+ ] ;
2281+ }
22442282}
22452283
22462284const encodeBase64 = ( input : string ) => {
0 commit comments