Skip to content

Commit aa0cef8

Browse files
authored
Always execute concurrent calls to exchange the refresh token serially (#30)
2 parents 281e288 + 971dcc0 commit aa0cef8

File tree

13 files changed

+235
-96
lines changed

13 files changed

+235
-96
lines changed

.github/workflows/build.yml

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,13 +112,23 @@ jobs:
112112
-scheme '${{ matrix.scheme }}'
113113
-configuration Debug
114114
-destination 'platform=${{ matrix.platform }}'
115-
-resultBundlePath ${{ env.TEST_RESULTS_PATH }}
116115
-showBuildTimingSummary
117-
build test
116+
build
117+
118+
- name: test
119+
if: success()
120+
continue-on-error: true
121+
run: >
122+
xcodebuild
123+
-scheme '${{ matrix.scheme }}'
124+
-configuration Debug
125+
-destination 'platform=${{ matrix.platform }}'
126+
-resultBundlePath ${{ env.TEST_RESULTS_PATH }}
127+
test
118128
119129
- name: run xcresulttool
120130
uses: slidoapp/[email protected]
121-
if: success() || failure()
131+
if: always()
122132
with:
123133
title: 'results-xcode-tests-${{ matrix.platform }}'
124134
path: ${{ env.TEST_RESULTS_PATH }}

OAuth2.xcodeproj/project.pbxproj

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@
3434
871279832DB152BB00A5AF72 /* SwiftKeychain in Frameworks */ = {isa = PBXBuildFile; productRef = 871279822DB152BB00A5AF72 /* SwiftKeychain */; };
3535
871279852DB7DD1B00A5AF72 /* OAuth2ExchangeAccessTokenForResourceTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 871279842DB7DD1B00A5AF72 /* OAuth2ExchangeAccessTokenForResourceTests.swift */; };
3636
8760AE592E15335E00020465 /* OAuth2MockPerformer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8760AE582E15335A00020465 /* OAuth2MockPerformer.swift */; };
37+
8760AE5C2E16C77700020465 /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = 8760AE5B2E16C77700020465 /* Semaphore */; };
38+
8760AE5E2E16C77C00020465 /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = 8760AE5D2E16C77C00020465 /* Semaphore */; };
39+
8760AE602E16C78000020465 /* Semaphore in Frameworks */ = {isa = PBXBuildFile; productRef = 8760AE5F2E16C78000020465 /* Semaphore */; };
3740
8793811929D483EC00DC4EBC /* OAuth2DeviceGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8793811829D483EC00DC4EBC /* OAuth2DeviceGrant.swift */; };
3841
8793811A29D483EC00DC4EBC /* OAuth2DeviceGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8793811829D483EC00DC4EBC /* OAuth2DeviceGrant.swift */; };
3942
8793811B29D483EC00DC4EBC /* OAuth2DeviceGrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8793811829D483EC00DC4EBC /* OAuth2DeviceGrant.swift */; };
@@ -261,6 +264,7 @@
261264
buildActionMask = 2147483647;
262265
files = (
263266
871279812DB152B300A5AF72 /* SwiftKeychain in Frameworks */,
267+
8760AE602E16C78000020465 /* Semaphore in Frameworks */,
264268
);
265269
runOnlyForDeploymentPostprocessing = 0;
266270
};
@@ -269,6 +273,7 @@
269273
buildActionMask = 2147483647;
270274
files = (
271275
8712797F2DB152AE00A5AF72 /* SwiftKeychain in Frameworks */,
276+
8760AE5E2E16C77C00020465 /* Semaphore in Frameworks */,
272277
);
273278
runOnlyForDeploymentPostprocessing = 0;
274279
};
@@ -277,6 +282,7 @@
277282
buildActionMask = 2147483647;
278283
files = (
279284
8712797D2DB152A800A5AF72 /* SwiftKeychain in Frameworks */,
285+
8760AE5C2E16C77700020465 /* Semaphore in Frameworks */,
280286
);
281287
runOnlyForDeploymentPostprocessing = 0;
282288
};
@@ -654,6 +660,7 @@
654660
mainGroup = EEDB861A193FAAE500C4EEA1;
655661
packageReferences = (
656662
8712797A2DB151ED00A5AF72 /* XCRemoteSwiftPackageReference "SwiftKeychain" */,
663+
8760AE5A2E16C5E800020465 /* XCRemoteSwiftPackageReference "Semaphore" */,
657664
);
658665
productRefGroup = EEDB8625193FAAE500C4EEA1 /* Products */;
659666
projectDirPath = "";
@@ -1274,6 +1281,14 @@
12741281
minimumVersion = 2.1.0;
12751282
};
12761283
};
1284+
8760AE5A2E16C5E800020465 /* XCRemoteSwiftPackageReference "Semaphore" */ = {
1285+
isa = XCRemoteSwiftPackageReference;
1286+
repositoryURL = "https://github.com/groue/Semaphore";
1287+
requirement = {
1288+
kind = upToNextMinorVersion;
1289+
minimumVersion = 0.1.0;
1290+
};
1291+
};
12771292
/* End XCRemoteSwiftPackageReference section */
12781293

12791294
/* Begin XCSwiftPackageProductDependency section */
@@ -1297,6 +1312,21 @@
12971312
package = 8712797A2DB151ED00A5AF72 /* XCRemoteSwiftPackageReference "SwiftKeychain" */;
12981313
productName = SwiftKeychain;
12991314
};
1315+
8760AE5B2E16C77700020465 /* Semaphore */ = {
1316+
isa = XCSwiftPackageProductDependency;
1317+
package = 8760AE5A2E16C5E800020465 /* XCRemoteSwiftPackageReference "Semaphore" */;
1318+
productName = Semaphore;
1319+
};
1320+
8760AE5D2E16C77C00020465 /* Semaphore */ = {
1321+
isa = XCSwiftPackageProductDependency;
1322+
package = 8760AE5A2E16C5E800020465 /* XCRemoteSwiftPackageReference "Semaphore" */;
1323+
productName = Semaphore;
1324+
};
1325+
8760AE5F2E16C78000020465 /* Semaphore */ = {
1326+
isa = XCSwiftPackageProductDependency;
1327+
package = 8760AE5A2E16C5E800020465 /* XCRemoteSwiftPackageReference "Semaphore" */;
1328+
productName = Semaphore;
1329+
};
13001330
/* End XCSwiftPackageProductDependency section */
13011331
};
13021332
rootObject = EEDB861B193FAAE500C4EEA1 /* Project object */;

OAuth2.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.resolved

Lines changed: 9 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,12 @@ let package = Package(
3131
],
3232
dependencies: [
3333
.package(url: "https://github.com/slidoapp/SwiftKeychain.git", .upToNextMinor(from: "2.1.0")),
34+
.package(url: "https://github.com/groue/Semaphore.git", .upToNextMinor(from: "0.1.0"))
3435
],
3536
targets: [
3637
.target(name: "OAuth2",
3738
dependencies: ["Base", "Flows", "DataLoader"]),
38-
.target(name: "Base", dependencies: ["SwiftKeychain"]),
39+
.target(name: "Base", dependencies: ["SwiftKeychain", "Semaphore"]),
3940
.target(name: "macOS", dependencies: [.target(name: "Base")]),
4041
.target(name: "iOS", dependencies: [.target(name: "Base")]),
4142
.target(name: "tvOS", dependencies: [.target(name: "Base")]),

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/OAuth2Base.swift

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,6 @@ open class OAuth2Base: OAuth2Securable {
134134
/// Returns true if the receiver is currently authorizing.
135135
public final var isAuthorizing: Bool = false
136136

137-
/// Returns true if the receiver is currently exchanging the refresh token.
138-
public final var isExchangingRefreshToken: Bool = false
139-
140137
/**
141138
Closure called after the regular authorization callback, on the main thread. You can use this callback when you're performing
142139
authorization manually and/or for cleanup operations.

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/Base/OAuth2Error.swift

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,6 @@ public enum OAuth2Error: Error, CustomStringConvertible, Equatable {
6464
/// The client is already authorizing.
6565
case alreadyAuthorizing
6666

67-
/// The client is already exchanging the refresh token.
68-
case alreadyExchangingRefreshToken
69-
7067
/// There is no authorization context.
7168
case noAuthorizationContext
7269

@@ -254,8 +251,6 @@ public enum OAuth2Error: Error, CustomStringConvertible, Equatable {
254251
return "The password grant flow needs to be set a delegate to present the login controller."
255252
case .alreadyAuthorizing:
256253
return "The client is already authorizing, wait for it to finish or abort authorization before trying again"
257-
case .alreadyExchangingRefreshToken:
258-
return "The client is already exchanging the refresh token, wait for it to finish before trying again"
259254
case .noAuthorizationContext:
260255
return "No authorization context present"
261256
case .invalidAuthorizationContext:
@@ -347,7 +342,6 @@ public enum OAuth2Error: Error, CustomStringConvertible, Equatable {
347342
case (.noUsername, .noUsername): return true
348343
case (.noPassword, .noPassword): return true
349344
case (.alreadyAuthorizing, .alreadyAuthorizing): return true
350-
case (.alreadyExchangingRefreshToken, .alreadyExchangingRefreshToken): return true
351345
case (.noAuthorizationContext, .noAuthorizationContext): return true
352346
case (.invalidAuthorizationContext, .invalidAuthorizationContext): return true
353347
case (.invalidAuthorizationConfiguration(let l), .invalidAuthorizationConfiguration(let r)): return l == r

0 commit comments

Comments
 (0)