Skip to content

Commit 971dcc0

Browse files
committed
Add setting allowing to configure it Refresh Token Rotation is enabled for auth client
1 parent 5ba88d1 commit 971dcc0

File tree

3 files changed

+43
-10
lines changed

3 files changed

+43
-10
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -483,6 +483,12 @@ PKCE
483483
PKCE support is controlled by the `useProofKeyForCodeExchange` property, and the `use_pkce` key in the settings dictionary.
484484
It is disabled by default. When enabled, a new code verifier string is generated for every authorization request.
485485

486+
Refresh Token Rotation
487+
----------------------
488+
489+
Refresh Token Rotation setting is controlled by the `refreshTokenRotationIsEnabled` property, and the `refresh_token_rotation` key in the settings dictionary.
490+
It is enabled by default. When enabled, all calls that could rotate the refresh token are executed sequentially to ensure that only the most recently rotated refresh token is persisted.
491+
486492

487493
Keychain
488494
--------

Sources/Base/OAuth2ClientConfig.swift

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,17 @@ open class OAuth2ClientConfig {
102102
/// See https://tools.ietf.org/html/rfc7636
103103
///
104104
open var useProofKeyForCodeExchange = false
105+
106+
107+
/// If the refresh token rotation is enabled, the authorization server issues a new refresh token with every access token refresh response (the previous refresh token is invalidated).
108+
///
109+
/// We need to know whether this functionality is enabled on the auth server to prevent concurrent calls to any operations that could rotate the refresh token.
110+
/// If the refresh token rotation is enabled, these calls must always be executed sequentially to ensure that only the most recently rotated refresh token is persisted.
111+
///
112+
/// Read more about it here:
113+
/// https://datatracker.ietf.org/doc/html/rfc9700#name-refresh-token-protection
114+
/// https://auth0.com/docs/secure/tokens/refresh-tokens/refresh-token-rotation
115+
open var refreshTokenRotationIsEnabled = true
105116

106117
/// Optional custom User-Agent string for embedded mode.
107118
open var customUserAgent: String?
@@ -172,6 +183,10 @@ open class OAuth2ClientConfig {
172183
useProofKeyForCodeExchange = usePKCE
173184
}
174185

186+
if let refreshTokenRotation = settings["refresh_token_rotation"] as? Bool {
187+
refreshTokenRotationIsEnabled = refreshTokenRotation
188+
}
189+
175190
customUserAgent = settings["custom_user_agent"] as? String
176191
}
177192

Sources/Flows/OAuth2.swift

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ open class OAuth2: OAuth2Base {
5050
/// The authorizer to use for UI handling, depending on platform.
5151
open var authorizer: OAuth2AuthorizerUI!
5252

53-
/// The semaphore preventing concurrent execution of `doExchangeRefreshToken()` function.
54-
private let exchangeRefreshTokenSemaphore = AsyncSemaphore(value: 1)
53+
/// The semaphore preventing concurrent execution of any function that could rotate the refresh token.
54+
private var refreshTokenRotationSemaphore: AsyncSemaphore?
5555

5656

5757
/**
@@ -85,7 +85,11 @@ open class OAuth2: OAuth2Base {
8585
*/
8686
override public init(settings: OAuth2JSON) {
8787
super.init(settings: settings)
88-
authorizer = OAuth2Authorizer(oauth2: self)
88+
self.authorizer = OAuth2Authorizer(oauth2: self)
89+
90+
if (self.clientConfig.refreshTokenRotationIsEnabled) {
91+
self.refreshTokenRotationSemaphore = AsyncSemaphore(value: 1)
92+
}
8993
}
9094

9195

@@ -109,9 +113,6 @@ open class OAuth2: OAuth2Base {
109113
guard !self.isAuthorizing else {
110114
throw OAuth2Error.alreadyAuthorizing
111115
}
112-
113-
/// Wait for all running exchanges to finish
114-
await self.exchangeRefreshTokenSemaphore.wait()
115116

116117
self.isAuthorizing = true
117118
logger?.debug("OAuth2", msg: "Starting authorization")
@@ -347,6 +348,12 @@ open class OAuth2: OAuth2Base {
347348
- returns: OAuth2 JSON dictionary
348349
*/
349350
open func doRefreshToken(params: OAuth2StringDict? = nil) async throws -> OAuth2JSON {
351+
/// Wait for all running rotations to finish
352+
await self.refreshTokenRotationSemaphore?.wait()
353+
defer {
354+
self.refreshTokenRotationSemaphore?.signal()
355+
}
356+
350357
do {
351358
let post = try tokenRequestForTokenRefresh(params: params).asURLRequest(for: self)
352359
logger?.debug("OAuth2", msg: "Using refresh token to receive access token from \(post.url?.description ?? "nil")")
@@ -407,10 +414,10 @@ open class OAuth2: OAuth2Base {
407414
- returns: Exchanged refresh token
408415
*/
409416
open func doExchangeRefreshToken(audienceClientId: String, traceId: String, params: OAuth2StringDict? = nil) async throws -> String {
410-
/// Make sure no two tasks can execute `doExchangeRefreshToken()` concurrently.
411-
await exchangeRefreshTokenSemaphore.wait()
417+
/// Wait for all running rotations to finish
418+
await self.refreshTokenRotationSemaphore?.wait()
412419
defer {
413-
exchangeRefreshTokenSemaphore.signal()
420+
self.refreshTokenRotationSemaphore?.signal()
414421
}
415422

416423
debugPrint("[doExchangeRefreshToken] Started for \(audienceClientId)")
@@ -481,14 +488,19 @@ open class OAuth2: OAuth2Base {
481488
return req
482489
}
483490

484-
// TODO:
485491
/**
486492
Exchanges the access token for resource access token.
487493

488494
- parameter params: Optional key/value pairs to pass during token exchange
489495
- returns: Exchanged access token
490496
*/
491497
open func doExchangeAccessTokenForResource(params: OAuth2StringDict? = nil) async throws -> String {
498+
/// Wait for all running rotations to finish
499+
await self.refreshTokenRotationSemaphore?.wait()
500+
defer {
501+
self.refreshTokenRotationSemaphore?.signal()
502+
}
503+
492504
do {
493505
guard let resourceURIs = clientConfig.resourceURIs, !resourceURIs.isEmpty else {
494506
throw OAuth2Error.noResourceURI

0 commit comments

Comments
 (0)