From a371aafd06373b6d5b6d0e8eb6d5ed0369f1d559 Mon Sep 17 00:00:00 2001 From: mohssenfathi Date: Tue, 10 Sep 2024 12:50:14 -0700 Subject: [PATCH] Logout API --- Sources/UberAuth/AuthProviding.swift | 4 + .../AuthorizationCodeAuthProvider.swift | 8 + Sources/UberAuth/UberAuth.swift | 41 +++- examples/UberSDK/UberSDK/ContentView.swift | 22 +- .../UberSDK/UberSDKTests/Mocks/Mocks.swift | 200 ++++++++++-------- .../UberAuth/AuthManagerTests.swift | 61 ++++++ 6 files changed, 245 insertions(+), 91 deletions(-) diff --git a/Sources/UberAuth/AuthProviding.swift b/Sources/UberAuth/AuthProviding.swift index f5ff74c..eb6b99b 100644 --- a/Sources/UberAuth/AuthProviding.swift +++ b/Sources/UberAuth/AuthProviding.swift @@ -33,7 +33,11 @@ public protocol AuthProviding { prefill: Prefill?, completion: @escaping (Result) -> ()) + func logout() -> Bool + func handle(response url: URL) -> Bool + + var isLoggedIn: Bool { get } } extension AuthProviding where Self == AuthorizationCodeAuthProvider { diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index dc9213f..8b7d3ff 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -157,6 +157,10 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.completion = authCompletion } + public func logout() -> Bool { + tokenManager.deleteToken(identifier: TokenManager.defaultAccessTokenIdentifier) + } + public func handle(response url: URL) -> Bool { guard responseParser.isValidResponse(url: url, matching: redirectURI) else { return false @@ -168,6 +172,10 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { return true } + public var isLoggedIn: Bool { + tokenManager.getToken(identifier: TokenManager.defaultAccessTokenIdentifier) != nil + } + // MARK: - Private private func executeLogin(authDestination: AuthDestination, diff --git a/Sources/UberAuth/UberAuth.swift b/Sources/UberAuth/UberAuth.swift index 8e61f3d..ba69d47 100644 --- a/Sources/UberAuth/UberAuth.swift +++ b/Sources/UberAuth/UberAuth.swift @@ -32,7 +32,11 @@ protocol AuthManaging { func login(context: AuthContext, completion: @escaping AuthCompletion) + func logout() + func handle(_ url: URL) -> Bool + + var isLoggedIn: Bool { get } } /// Public interface for the uber-auth-ios library @@ -53,6 +57,13 @@ public final class UberAuth: AuthManaging { ) } + /// Clears any saved auth information from the keychain + /// If `currentAuthContext` exists, logs out using the stored auth context + /// Otherwise, attempts to delete the saved auth token directly using the internal TokenManager + public static func logout() { + auth.logout() + } + /// Attempts to extract auth information from the provided URL. /// This method should be called from the implemeting application's openURL function. /// @@ -63,9 +74,22 @@ public final class UberAuth: AuthManaging { auth.handle(url) } + /// A computed property that indicates if auth information is saved in the keychain + /// First checks for saved token information using the current auth provider + /// If no auth provider exists, falls back to the default token identifier + public static var isLoggedIn: Bool { + auth.isLoggedIn + } + // MARK: Internal // MARK: AuthManaging + init(currentContext: AuthContext? = nil, + tokenManager: TokenManaging = TokenManager()) { + self.currentContext = currentContext + self.tokenManager = tokenManager + } + func login(context: AuthContext = .init(), completion: @escaping AuthCompletion) { context.authProvider.execute( @@ -76,6 +100,15 @@ public final class UberAuth: AuthManaging { currentContext = context } + func logout() { + guard let currentContext else { + tokenManager.deleteToken(identifier: TokenManager.defaultAccessTokenIdentifier) + return + } + currentContext.authProvider.logout() + self.currentContext = nil + } + func handle(_ url: URL) -> Bool { guard let currentContext else { return false @@ -83,8 +116,14 @@ public final class UberAuth: AuthManaging { return currentContext.authProvider.handle(response: url) } + var isLoggedIn: Bool { + currentContext?.authProvider.isLoggedIn ?? (tokenManager.getToken(identifier: TokenManager.defaultAccessTokenIdentifier) != nil) + } + private static let auth = UberAuth() - private var currentContext: AuthContext? + var currentContext: AuthContext? + + var tokenManager: TokenManaging = TokenManager() } diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index 2ddb968..3fe2082 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -54,16 +54,19 @@ final class Content { var isPrefillExpanded: Bool = false var response: AuthReponse? var prefillBuilder = PrefillBuilder() + var isLoggedIn: Bool { + UberAuth.isLoggedIn + } func login() { - var promt: Prompt = [] - if shouldForceLogin { promt.insert(.login) } - if shouldForceConsent { promt.insert(.consent) } + var prompt: Prompt = [] + if shouldForceLogin { prompt.insert(.login) } + if shouldForceConsent { prompt.insert(.consent) } let authProvider: AuthProviding = .authorizationCode( shouldExchangeAuthCode: isTokenExchangeEnabled, - prompt: promt + prompt: prompt ) let authDestination: AuthDestination = { @@ -94,6 +97,11 @@ final class Content { ) } + func logout() { + UberAuth.logout() + response = nil + } + func openUrl(_ url: URL) { UberAuth.handle(url) } @@ -219,9 +227,11 @@ struct ContentView: View { } Button( - action: { content.login() }, + action: { + content.isLoggedIn ? content.logout() : content.login() + }, label: { - Text("Login") + Text(content.isLoggedIn ? "Logout" : "Login") .frame(maxWidth: .infinity, alignment: .center) } ) diff --git a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift index c0258ec..3f07bb5 100644 --- a/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift +++ b/examples/UberSDK/UberSDKTests/Mocks/Mocks.swift @@ -11,28 +11,38 @@ import UIKit @testable import UberCore -class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { - init() { } +public class TokenManagingMock: TokenManaging { + public init() { } - private(set) var isValidResponseCallCount = 0 - var isValidResponseHandler: ((URL, String) -> (Bool))? - func isValidResponse(url: URL, matching redirectURI: String) -> Bool { - isValidResponseCallCount += 1 - if let isValidResponseHandler = isValidResponseHandler { - return isValidResponseHandler(url, redirectURI) + public private(set) var saveTokenCallCount = 0 + public var saveTokenHandler: ((AccessToken, String, String?) -> (Bool))? + public func saveToken(_ token: AccessToken, identifier: String, accessGroup: String?) -> Bool { + saveTokenCallCount += 1 + if let saveTokenHandler = saveTokenHandler { + return saveTokenHandler(token, identifier, accessGroup) } return false } - private(set) var callAsFunctionCallCount = 0 - var callAsFunctionHandler: ((URL) -> (Result))? - func callAsFunction(url: URL) -> Result { - callAsFunctionCallCount += 1 - if let callAsFunctionHandler = callAsFunctionHandler { - return callAsFunctionHandler(url) + public private(set) var getTokenCallCount = 0 + public var getTokenHandler: ((String, String?) -> (AccessToken?))? + public func getToken(identifier: String, accessGroup: String?) -> AccessToken? { + getTokenCallCount += 1 + if let getTokenHandler = getTokenHandler { + return getTokenHandler(identifier, accessGroup) } - fatalError("callAsFunctionHandler returns can't have a default value thus its handler must be set") + return nil + } + + public private(set) var deleteTokenCallCount = 0 + public var deleteTokenHandler: ((String, String?) -> (Bool))? + public func deleteToken(identifier: String, accessGroup: String?) -> Bool { + deleteTokenCallCount += 1 + if let deleteTokenHandler = deleteTokenHandler { + return deleteTokenHandler(identifier, accessGroup) + } + return false } } @@ -51,6 +61,66 @@ class NetworkProvidingMock: NetworkProviding { } } +public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { + public init() { } + + + public private(set) var saveCallCount = 0 + public var saveHandler: ((Any, String, String?) -> (Bool))? + public func save(_ value: V, for key: String, accessGroup: String?) -> Bool { + saveCallCount += 1 + if let saveHandler = saveHandler { + return saveHandler(value, key, accessGroup) + } + return false + } + + public private(set) var getCallCount = 0 + public var getHandler: ((String, String?) -> (Any?))? + public func get(key: String, accessGroup: String?) -> V? { + getCallCount += 1 + if let getHandler = getHandler { + return getHandler(key, accessGroup) as? V + } + return nil + } + + public private(set) var deleteCallCount = 0 + public var deleteHandler: ((String, String?) -> (Bool))? + public func delete(key: String, accessGroup: String?) -> Bool { + deleteCallCount += 1 + if let deleteHandler = deleteHandler { + return deleteHandler(key, accessGroup) + } + return false + } +} + +class AuthorizationCodeResponseParsingMock: AuthorizationCodeResponseParsing { + init() { } + + + private(set) var isValidResponseCallCount = 0 + var isValidResponseHandler: ((URL, String) -> (Bool))? + func isValidResponse(url: URL, matching redirectURI: String) -> Bool { + isValidResponseCallCount += 1 + if let isValidResponseHandler = isValidResponseHandler { + return isValidResponseHandler(url, redirectURI) + } + return false + } + + private(set) var callAsFunctionCallCount = 0 + var callAsFunctionHandler: ((URL) -> (Result))? + func callAsFunction(url: URL) -> Result { + callAsFunctionCallCount += 1 + if let callAsFunctionHandler = callAsFunctionHandler { + return callAsFunctionHandler(url) + } + fatalError("callAsFunctionHandler returns can't have a default value thus its handler must be set") + } +} + public class ApplicationLaunchingMock: ApplicationLaunching { public init() { } @@ -133,6 +203,9 @@ class AuthenticationSessioningMock: AuthenticationSessioning { public class AuthProvidingMock: AuthProviding { public init() { } + public init(isLoggedIn: Bool = false) { + self.isLoggedIn = isLoggedIn + } public private(set) var executeCallCount = 0 @@ -145,6 +218,16 @@ public class AuthProvidingMock: AuthProviding { } + public private(set) var logoutCallCount = 0 + public var logoutHandler: (() -> (Bool))? + public func logout() -> Bool { + logoutCallCount += 1 + if let logoutHandler = logoutHandler { + return logoutHandler() + } + return false + } + public private(set) var handleCallCount = 0 public var handleHandler: ((URL) -> (Bool))? public func handle(response url: URL) -> Bool { @@ -154,10 +237,16 @@ public class AuthProvidingMock: AuthProviding { } return false } + + public private(set) var isLoggedInSetCallCount = 0 + public var isLoggedIn: Bool = false { didSet { isLoggedInSetCallCount += 1 } } } class AuthManagingMock: AuthManaging { init() { } + init(isLoggedIn: Bool = false) { + self.isLoggedIn = isLoggedIn + } private(set) var loginCallCount = 0 @@ -170,6 +259,16 @@ class AuthManagingMock: AuthManaging { } + private(set) var logoutCallCount = 0 + var logoutHandler: (() -> ())? + func logout() { + logoutCallCount += 1 + if let logoutHandler = logoutHandler { + logoutHandler() + } + + } + private(set) var handleCallCount = 0 var handleHandler: ((URL) -> (Bool))? func handle(_ url: URL) -> Bool { @@ -179,75 +278,8 @@ class AuthManagingMock: AuthManaging { } return false } -} - -public class TokenManagingMock: TokenManaging { - public init() { } - - - public private(set) var saveTokenCallCount = 0 - public var saveTokenHandler: ((AccessToken, String, String?) -> (Bool))? - public func saveToken(_ token: AccessToken, identifier: String, accessGroup: String?) -> Bool { - saveTokenCallCount += 1 - if let saveTokenHandler = saveTokenHandler { - return saveTokenHandler(token, identifier, accessGroup) - } - return false - } - - public private(set) var getTokenCallCount = 0 - public var getTokenHandler: ((String, String?) -> (AccessToken?))? - public func getToken(identifier: String, accessGroup: String?) -> AccessToken? { - getTokenCallCount += 1 - if let getTokenHandler = getTokenHandler { - return getTokenHandler(identifier, accessGroup) - } - return nil - } - - public private(set) var deleteTokenCallCount = 0 - public var deleteTokenHandler: ((String, String?) -> (Bool))? - public func deleteToken(identifier: String, accessGroup: String?) -> Bool { - deleteTokenCallCount += 1 - if let deleteTokenHandler = deleteTokenHandler { - return deleteTokenHandler(identifier, accessGroup) - } - return false - } -} -public class KeychainUtilityProtocolMock: KeychainUtilityProtocol { - public init() { } - - - public private(set) var saveCallCount = 0 - public var saveHandler: ((Any, String, String?) -> (Bool))? - public func save(_ value: V, for key: String, accessGroup: String?) -> Bool { - saveCallCount += 1 - if let saveHandler = saveHandler { - return saveHandler(value, key, accessGroup) - } - return false - } - - public private(set) var getCallCount = 0 - public var getHandler: ((String, String?) -> (Any?))? - public func get(key: String, accessGroup: String?) -> V? { - getCallCount += 1 - if let getHandler = getHandler { - return getHandler(key, accessGroup) as? V - } - return nil - } - - public private(set) var deleteCallCount = 0 - public var deleteHandler: ((String, String?) -> (Bool))? - public func delete(key: String, accessGroup: String?) -> Bool { - deleteCallCount += 1 - if let deleteHandler = deleteHandler { - return deleteHandler(key, accessGroup) - } - return false - } + private(set) var isLoggedInSetCallCount = 0 + var isLoggedIn: Bool = false { didSet { isLoggedInSetCallCount += 1 } } } diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift index 11848ea..3c084e8 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthManagerTests.swift @@ -243,4 +243,65 @@ final class UberAuthTests: XCTestCase { XCTAssertEqual(authProvider.handleCallCount, 1) } + + func test_isLoggedIn_noCurrentContext_returnsFalseIfNoToken() { + UberAuth.logout() + XCTAssertFalse(UberAuth.isLoggedIn) + } + + func test_isLoggedIn_callsCurrentContextIsLoggedIn() { + let authProvider = AuthProvidingMock() + + let context = AuthContext( + authDestination: .inApp, + authProvider: authProvider, + prefill: nil + ) + + UberAuth.login( + context: context, + completion: { _ in } + ) + + XCTAssertFalse(UberAuth.isLoggedIn) + + authProvider.isLoggedIn = true + + XCTAssertTrue(UberAuth.isLoggedIn) + } + + func test_logout_triggersAuthProviderLogout() { + let tokenManager = TokenManagingMock() + let auth = UberAuth(tokenManager: tokenManager) + let authProvider = AuthProvidingMock() + + let context = AuthContext( + authDestination: .inApp, + authProvider: authProvider, + prefill: nil + ) + + auth.login( + context: context, + completion: { _ in } + ) + + XCTAssertNotNil(auth.currentContext) + XCTAssertEqual(authProvider.logoutCallCount, 0) + + auth.logout() + + XCTAssertEqual(authProvider.logoutCallCount, 1) + } + + func test_logout_noCurrentContext_deletesToken() { + let tokenManager = TokenManagingMock() + let auth = UberAuth(tokenManager: tokenManager) + + XCTAssertEqual(tokenManager.deleteTokenCallCount, 0) + + auth.logout() + + XCTAssertEqual(tokenManager.deleteTokenCallCount, 1) + } }