Skip to content

Commit 15ff6ae

Browse files
authored
refactor: updated the key logic for storing MRRT token (#1031)
1 parent aa4dcd8 commit 15ff6ae

File tree

2 files changed

+194
-22
lines changed

2 files changed

+194
-22
lines changed

Auth0/CredentialsManager.swift

Lines changed: 52 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public struct CredentialsManager {
3737
let noSession: TimeInterval = -1
3838
var lastBiometricAuthTime: TimeInterval = -1
3939
let lock = NSLock()
40-
40+
4141
init() {
4242
lastBiometricAuthTime = noSession
4343
}
@@ -163,9 +163,12 @@ public struct CredentialsManager {
163163
/// ```
164164
///
165165
/// - Parameter audience: Identifier of the API the stored API credentials are for.
166+
/// - Parameter scope: Optional scope for which the API Credentials are stored. If the credentials were initially fetched/stored with scope,
167+
/// it is recommended to pass scope also while clearing them.
166168
/// - Returns: If the API credentials were removed.
167-
public func clear(forAudience audience: String) -> Bool {
168-
return self.storage.deleteEntry(forKey: audience)
169+
public func clear(forAudience audience: String, scope: String? = nil) -> Bool {
170+
let key = getAPICredentialsStorageKey(audience: audience, scope: scope)
171+
return self.storage.deleteEntry(forKey: key)
169172
}
170173

171174
#if WEB_AUTH_PLATFORM
@@ -180,13 +183,13 @@ public struct CredentialsManager {
180183
/// - Returns: `true` if the session is valid and biometric authentication can be skipped, `false` otherwise.
181184
public func isBiometricSessionValid() -> Bool {
182185
guard let bioAuth = self.bioAuth else { return false }
183-
186+
184187
self.biometricSession.lock.lock()
185188
defer { self.biometricSession.lock.unlock() }
186-
189+
187190
let lastAuth = self.biometricSession.lastBiometricAuthTime
188191
if lastAuth == self.biometricSession.noSession { return false }
189-
192+
190193
switch bioAuth.policy {
191194
case .session(let timeoutInSeconds), .appLifecycle(let timeoutInSeconds):
192195
let timeoutInterval = TimeInterval(timeoutInSeconds)
@@ -694,24 +697,39 @@ public struct CredentialsManager {
694697
callback: callback)
695698
}
696699

697-
public func store(apiCredentials: APICredentials, forAudience audience: String) -> Bool {
700+
public func store(apiCredentials: APICredentials, forAudience audience: String, forScope scope: String? = nil) -> Bool {
698701
guard let data = try? apiCredentials.encode() else {
699702
return false
700703
}
701704

702-
return self.storage.setEntry(data, forKey: audience)
705+
let key = getAPICredentialsStorageKey(audience: audience, scope: scope)
706+
return self.storage.setEntry(data, forKey: key)
703707
}
704708

705709
private func retrieveCredentials() -> Credentials? {
706710
guard let data = self.storage.getEntry(forKey: self.storeKey) else { return nil }
707711
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Credentials.self, from: data)
708712
}
709713

710-
private func retrieveAPICredentials(audience: String) -> APICredentials? {
711-
guard let data = self.storage.getEntry(forKey: audience) else { return nil }
714+
private func retrieveAPICredentials(audience: String, scope: String?) -> APICredentials? {
715+
let key = getAPICredentialsStorageKey(audience: audience, scope: scope)
716+
guard let data = self.storage.getEntry(forKey: key) else { return nil }
712717
return try? APICredentials(from: data)
713718
}
714719

720+
private func getAPICredentialsStorageKey(audience: String, scope: String?) -> String {
721+
// Use audience if scope is null else use a combination of audience and scope
722+
if let scope = scope {
723+
let normalisedScopes = scope
724+
.split(separator: " ")
725+
.sorted()
726+
.joined(separator: "::")
727+
return "\(audience)::\(normalisedScopes)"
728+
} else {
729+
return audience
730+
}
731+
}
732+
715733
// swiftlint:disable:next function_parameter_count
716734
private func retrieveCredentials(scope: String?,
717735
minTTL: Int,
@@ -832,10 +850,10 @@ public struct CredentialsManager {
832850
dispatchGroup.enter()
833851

834852
DispatchQueue.global(qos: .userInitiated).async {
835-
if let apiCredentials = self.retrieveAPICredentials(audience: audience),
853+
if let apiCredentials = self.retrieveAPICredentials(audience: audience, scope: scope),
836854
!self.hasExpired(apiCredentials.expiresIn),
837855
!self.willExpire(apiCredentials.expiresIn, within: minTTL),
838-
!self.hasScopeChanged(from: apiCredentials.scope, to: scope) {
856+
!self.hasScopeChanged(from: apiCredentials.scope, to: scope, ignoreOpenid: scope?.contains("openid") == false) {
839857
dispatchGroup.leave()
840858
return callback(.success(apiCredentials))
841859
}
@@ -867,7 +885,7 @@ public struct CredentialsManager {
867885
} else if !self.store(credentials: newCredentials) {
868886
dispatchGroup.leave()
869887
callback(.failure(CredentialsManagerError(code: .storeFailed)))
870-
} else if !self.store(apiCredentials: newAPICredentials, forAudience: audience) {
888+
} else if !self.store(apiCredentials: newAPICredentials, forAudience: audience, forScope: scope) {
871889
dispatchGroup.leave()
872890
callback(.failure(CredentialsManagerError(code: .storeFailed)))
873891
} else {
@@ -893,14 +911,30 @@ public struct CredentialsManager {
893911
return expiresIn < Date()
894912
}
895913

896-
func hasScopeChanged(from lastScope: String?, to newScope: String?) -> Bool {
914+
func hasScopeChanged(from lastScope: String?, to newScope: String?, ignoreOpenid: Bool = false) -> Bool {
915+
897916
if let lastScope = lastScope, let newScope = newScope {
898-
let lastScopeList = lastScope.lowercased().split(separator: " ").sorted()
899-
let newScopeList = newScope.lowercased().split(separator: " ").sorted()
900917

901-
return lastScopeList != newScopeList
902-
}
918+
var storedScopes = Set(
919+
lastScope
920+
.split(separator: " ")
921+
.filter { !$0.isEmpty }
922+
.map { String($0).lowercased() }
923+
)
903924

925+
if ignoreOpenid {
926+
storedScopes.remove("openid")
927+
}
928+
929+
let requiredScopes = Set(
930+
newScope
931+
.split(separator: " ")
932+
.filter { !$0.isEmpty }
933+
.map { String($0).lowercased() }
934+
)
935+
936+
return storedScopes != requiredScopes
937+
}
904938
return false
905939
}
906940

Auth0Tests/CredentialsManagerSpec.swift

Lines changed: 142 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,79 @@ class CredentialsManagerSpec: QuickSpec {
127127

128128
}
129129

130+
describe("storage with scoped keys") {
131+
132+
afterEach {
133+
_ = credentialsManager.clear(forAudience: Audience)
134+
_ = credentialsManager.clear(forAudience: Audience, scope: Scope)
135+
_ = credentialsManager.clear(forAudience: Audience, scope: NewScope)
136+
_ = credentialsManager.clear(forAudience: Audience, scope: "read write")
137+
_ = credentialsManager.clear(forAudience: Audience, scope: "read")
138+
}
139+
140+
it("should store api credentials without scope using audience as key") {
141+
let store = SimpleKeychain()
142+
credentialsManager = CredentialsManager(authentication: authentication, storage: store)
143+
144+
expect(credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience)).to(beTrue())
145+
expect(fetchAPICredentials(forAudience: Audience, from: store)).toNot(beNil())
146+
}
147+
148+
it("should store api credentials with scope using compound key") {
149+
let store = SimpleKeychain()
150+
credentialsManager = CredentialsManager(authentication: authentication, storage: store)
151+
152+
expect(credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: "read write")).to(beTrue())
153+
154+
expect(fetchAPICredentials(forAudience: Audience, forScope: "read write", from: store)).toNot(beNil())
155+
}
156+
157+
it("should store credentials for same audience with different scopes separately") {
158+
let store = SimpleKeychain()
159+
credentialsManager = CredentialsManager(authentication: authentication, storage: store)
160+
161+
let apiCredentials1 = APICredentials(accessToken: "token1", tokenType: TokenType, expiresIn: Date(timeIntervalSinceNow: ExpiresIn), scope: "read")
162+
let apiCredentials2 = APICredentials(accessToken: "token2", tokenType: TokenType, expiresIn: Date(timeIntervalSinceNow: ExpiresIn), scope: "write")
163+
164+
expect(credentialsManager.store(apiCredentials: apiCredentials1, forAudience: Audience, forScope: "read")).to(beTrue())
165+
expect(credentialsManager.store(apiCredentials: apiCredentials2, forAudience: Audience, forScope: "write")).to(beTrue())
166+
167+
// Both should exist
168+
let retrieved1 = fetchAPICredentials(forAudience: Audience, forScope: "read", from: store)
169+
let retrieved2 = fetchAPICredentials(forAudience: Audience, forScope: "write", from: store)
170+
expect(retrieved1?.accessToken) == "token1"
171+
expect(retrieved2?.accessToken) == "token2"
172+
}
173+
174+
it("should clear api credentials only for specified scope") {
175+
let store = SimpleKeychain()
176+
credentialsManager = CredentialsManager(authentication: authentication, storage: store)
177+
178+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience)
179+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: "read")
180+
181+
_ = credentialsManager.clear(forAudience: Audience, scope: "read")
182+
183+
expect(fetchAPICredentials(forAudience: Audience, from: store)).toNot(beNil())
184+
185+
expect(fetchAPICredentials(forAudience: Audience, forScope: "read", from: store)).to(beNil())
186+
}
187+
188+
it("should not clear scoped credentials when clearing without scope") {
189+
let store = SimpleKeychain()
190+
credentialsManager = CredentialsManager(authentication: authentication, storage: store)
191+
192+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience)
193+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: "read")
194+
195+
_ = credentialsManager.clear(forAudience: Audience)
196+
197+
expect(fetchAPICredentials(forAudience: Audience, from: store)).to(beNil())
198+
expect(fetchAPICredentials(forAudience: Audience, forScope: "read", from: store)).toNot(beNil())
199+
}
200+
201+
}
202+
130203
describe("custom storage") {
131204

132205
class CustomStore: CredentialsStorage {
@@ -1122,7 +1195,7 @@ class CredentialsManagerSpec: QuickSpec {
11221195
tokenType: TokenType,
11231196
expiresIn: Date(timeIntervalSinceNow: ExpiresIn),
11241197
scope: "openid phone")
1125-
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience)
1198+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience,forScope: "openid phone")
11261199
waitUntil(timeout: Timeout) { done in
11271200
credentialsManager.apiCredentials(forAudience: Audience, scope: "openid phone") { result in
11281201
expect(result).to(haveAPICredentials(AccessToken))
@@ -1140,7 +1213,7 @@ class CredentialsManagerSpec: QuickSpec {
11401213
tokenType: TokenType,
11411214
expiresIn: Date(timeIntervalSinceNow: ExpiresIn),
11421215
scope: "openid phone")
1143-
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience)
1216+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: "openid phone")
11441217
waitUntil(timeout: Timeout) { done in
11451218
credentialsManager.apiCredentials(forAudience: Audience, scope: "openid email") { result in
11461219
expect(result).to(haveAPICredentials(NewAccessToken))
@@ -1185,6 +1258,64 @@ class CredentialsManagerSpec: QuickSpec {
11851258

11861259
}
11871260

1261+
context("retrieval of api credentials with scope") {
1262+
1263+
beforeEach {
1264+
_ = credentialsManager.store(credentials: credentials)
1265+
}
1266+
1267+
afterEach {
1268+
_ = credentialsManager.clear()
1269+
_ = credentialsManager.clear(forAudience: Audience)
1270+
_ = credentialsManager.clear(forAudience: Audience, scope: Scope)
1271+
_ = credentialsManager.clear(forAudience: Audience, scope: "openid phone")
1272+
_ = credentialsManager.clear(forAudience: Audience, scope: "different")
1273+
_ = credentialsManager.clear(forAudience: Audience, scope: "read write")
1274+
}
1275+
1276+
it("should retrieve api credentials stored with matching scope") {
1277+
apiCredentials = APICredentials(accessToken: AccessToken, tokenType: TokenType, expiresIn: Date(timeIntervalSinceNow: ExpiresIn), scope: Scope)
1278+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: Scope)
1279+
1280+
waitUntil(timeout: Timeout) { done in
1281+
credentialsManager.apiCredentials(forAudience: Audience, scope: Scope) { result in
1282+
expect(result).to(haveAPICredentials(AccessToken))
1283+
done()
1284+
}
1285+
}
1286+
}
1287+
1288+
it("should renew api credentials when scope does not match stored scope") {
1289+
NetworkStub.clearStubs()
1290+
NetworkStub.addStub(condition: {
1291+
$0.isToken(Domain) && $0.hasAtLeast(["refresh_token": RefreshToken, "audience": Audience])
1292+
}, response: authResponse(accessToken: NewAccessToken, idToken: NewIdToken, expiresIn: ExpiresIn, scope: "different"))
1293+
1294+
apiCredentials = APICredentials(accessToken: AccessToken, tokenType: TokenType, expiresIn: Date(timeIntervalSinceNow: ExpiresIn), scope: Scope)
1295+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: Scope)
1296+
1297+
waitUntil(timeout: Timeout) { done in
1298+
credentialsManager.apiCredentials(forAudience: Audience, scope: "different") { result in
1299+
expect(result).to(haveAPICredentials(NewAccessToken))
1300+
done()
1301+
}
1302+
}
1303+
}
1304+
1305+
it("should retrieve api credentials with scopes in different order") {
1306+
apiCredentials = APICredentials(accessToken: AccessToken, tokenType: TokenType, expiresIn: Date(timeIntervalSinceNow: ExpiresIn), scope: "read write")
1307+
_ = credentialsManager.store(apiCredentials: apiCredentials, forAudience: Audience, forScope: "read write")
1308+
1309+
waitUntil(timeout: Timeout) { done in
1310+
credentialsManager.apiCredentials(forAudience: Audience, scope: "write read") { result in
1311+
expect(result).to(haveAPICredentials(AccessToken))
1312+
done()
1313+
}
1314+
}
1315+
}
1316+
1317+
}
1318+
11881319
context("serial exchange for api credentials from same thread") {
11891320

11901321
it("should yield the stored api credentials after the previous renewal operation succeeded") {
@@ -2442,7 +2573,14 @@ private func fetchCredentials(from store: CredentialsStorage) -> Credentials? {
24422573
return try? NSKeyedUnarchiver.unarchivedObject(ofClass: Credentials.self, from: data)
24432574
}
24442575

2445-
private func fetchAPICredentials(forAudience audience: String = Audience, from store: CredentialsStorage) -> APICredentials? {
2446-
guard let data = store.getEntry(forKey: audience) else { return nil }
2576+
private func fetchAPICredentials(forAudience audience: String = Audience, forScope scope: String? = nil, from store: CredentialsStorage) -> APICredentials? {
2577+
let key: String
2578+
if let scope = scope {
2579+
let normalisedScopes = scope.split(separator: " ").sorted().joined(separator: "::")
2580+
key = "\(audience)::\(normalisedScopes)"
2581+
} else {
2582+
key = audience
2583+
}
2584+
guard let data = store.getEntry(forKey: key) else { return nil }
24472585
return try? APICredentials(from: data)
24482586
}

0 commit comments

Comments
 (0)