diff --git a/Auth0/CredentialsManager.swift b/Auth0/CredentialsManager.swift index 3f024ecbe..39fa77b95 100644 --- a/Auth0/CredentialsManager.swift +++ b/Auth0/CredentialsManager.swift @@ -29,6 +29,7 @@ public struct CredentialsManager { private let storage: CredentialsStorage private let storeKey: String private let authentication: Authentication + private let allowsAutoRefreshing: Bool private let dispatchQueue = DispatchQueue(label: "com.auth0.credentialsmanager.serial") #if WEB_AUTH_PLATFORM var bioAuth: BioAuthentication? @@ -40,12 +41,15 @@ public struct CredentialsManager { /// - authentication: Auth0 Authentication API client. /// - storeKey: Key used to store user credentials in the Keychain. Defaults to 'credentials'. /// - storage: The ``CredentialsStorage`` instance used to manage credentials storage. Defaults to a standard `SimpleKeychain` instance. + /// - allowsAutoRefreshing: If `true` (the default), `CredentialsManager` will automatically attempt to refresh credentials using a refresh token. public init(authentication: Authentication, storeKey: String = "credentials", - storage: CredentialsStorage = SimpleKeychain()) { + storage: CredentialsStorage = SimpleKeychain(), + allowsAutoRefreshing: Bool = true) { self.storeKey = storeKey self.authentication = authentication self.storage = storage + self.allowsAutoRefreshing = allowsAutoRefreshing } /// Retrieves the user information from the Keychain synchronously, without checking if the credentials are expired. @@ -240,12 +244,13 @@ public struct CredentialsManager { /// - Returns: If there are credentials stored containing a refresh token. public func canRenew() -> Bool { guard let credentials = self.retrieveCredentials() else { return false } - return credentials.refreshToken != nil + return self.allowsAutoRefreshing && credentials.refreshToken != nil } #if WEB_AUTH_PLATFORM - /// Retrieves credentials from the Keychain and automatically renews them using the refresh token if the access - /// token is expired. Otherwise, the retrieved credentials will be returned via the success case as they are still + /// Retrieves credentials from the Keychain and automatically renews them (if `allowsAutoRefreshing` is true) + /// using the refresh token if the access token is expired. + /// Otherwise, the retrieved credentials will be returned via the success case as they are still /// valid. Renewed credentials will be stored in the Keychain. **This method is thread-safe**. /// /// ## Usage @@ -652,6 +657,11 @@ public struct CredentialsManager { dispatchGroup.leave() return callback(.success(credentials)) } + + guard self.allowsAutoRefreshing else { + dispatchGroup.leave() + return callback(.failure(.renewNotSupported)) + } guard let refreshToken = credentials.refreshToken else { dispatchGroup.leave() return callback(.failure(.noRefreshToken)) diff --git a/Auth0/CredentialsManagerError.swift b/Auth0/CredentialsManagerError.swift index a7d4e901e..ef4a19ce7 100644 --- a/Auth0/CredentialsManagerError.swift +++ b/Auth0/CredentialsManagerError.swift @@ -7,6 +7,7 @@ public struct CredentialsManagerError: Auth0Error, Sendable { case noCredentials case noRefreshToken case renewFailed + case renewNotSupported case apiExchangeFailed case ssoExchangeFailed case storeFailed @@ -46,6 +47,10 @@ public struct CredentialsManagerError: Auth0Error, Sendable { /// The underlying ``AuthenticationError`` can be accessed via the ``Auth0Error/cause-9wuyi`` property. public static let renewFailed: CredentialsManagerError = .init(code: .renewFailed) + /// The credentials renewal is not supported. + /// The underlying ``AuthenticationError`` can be accessed via the ``Auth0Error/cause-9wuyi`` property. + public static let renewNotSupported: CredentialsManagerError = .init(code: .renewNotSupported) + /// The exchange of the refresh token for API credentials failed. /// The underlying ``AuthenticationError`` can be accessed via the ``Auth0Error/cause-9wuyi`` property. public static let apiExchangeFailed: CredentialsManagerError = .init(code: .apiExchangeFailed) @@ -82,6 +87,7 @@ extension CredentialsManagerError { case .noCredentials: return "No credentials were found in the store." case .noRefreshToken: return "The stored credentials instance does not contain a refresh token." case .renewFailed: return "The credentials renewal failed." + case .renewNotSupported: return "Credentials renewal is disabled." case .apiExchangeFailed: return "The exchange of the refresh token for API credentials failed." case .ssoExchangeFailed: return "The exchange of the refresh token for SSO credentials failed." case .storeFailed: return "Storing the renewed credentials failed." diff --git a/Auth0Tests/CredentialsManagerErrorSpec.swift b/Auth0Tests/CredentialsManagerErrorSpec.swift index 060a00c81..261a3f248 100644 --- a/Auth0Tests/CredentialsManagerErrorSpec.swift +++ b/Auth0Tests/CredentialsManagerErrorSpec.swift @@ -97,6 +97,12 @@ class CredentialsManagerErrorSpec: QuickSpec { expect(error.localizedDescription) == message } + it("should return message for renew failed") { + let message = "Credentials renewal is disabled." + let error = CredentialsManagerError(code: .renewNotSupported) + expect(error.localizedDescription) == message + } + it("should return message for API exchange failed") { let message = "The exchange of the refresh token for API credentials failed." let error = CredentialsManagerError(code: .apiExchangeFailed) diff --git a/Auth0Tests/CredentialsManagerSpec.swift b/Auth0Tests/CredentialsManagerSpec.swift index 2e3591c06..1851f8dbb 100644 --- a/Auth0Tests/CredentialsManagerSpec.swift +++ b/Auth0Tests/CredentialsManagerSpec.swift @@ -666,6 +666,21 @@ class CredentialsManagerSpec: QuickSpec { } } } + + it("should not renew if not enabled") { + credentialsManager = CredentialsManager(authentication: authentication, + storage: SimpleKeychain(), + allowsAutoRefreshing: false) + credentials = Credentials(accessToken: AccessToken, tokenType: TokenType, idToken: IdToken, refreshToken: RefreshToken, expiresIn: Date(timeIntervalSinceNow: -ExpiresIn)) + _ = credentialsManager.store(credentials: credentials) + + waitUntil(timeout: Timeout) { done in + credentialsManager.credentials { result in + expect(result).to(haveCredentialsManagerError(.renewNotSupported)) + done() + } + } + } } context("forced renewal of credentials") {