source/UberCore/Authentication/LoginManager.swift (254 lines of code) (raw):

// // LoginManager.swift // UberRides // // Copyright © 2016 Uber Technologies, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import SafariServices /// Manages user login via SSO, authorization code grant, or implicit grant. @objc(UBSDKLoginManager) public class LoginManager: NSObject, LoginManaging { private(set) public var accessTokenIdentifier: String private(set) public var keychainAccessGroup: String private(set) public var loginType: LoginType private(set) public var productFlowPriority: [UberAuthenticationProductFlow] private var oauthViewController: UIViewController? private var safariAuthenticationSession: Any? // Any? because otherwise this won't compile for earlier versions of iOS var authenticator: UberAuthenticating? var loggingIn: Bool = false var willEnterForegroundCalled: Bool = false private var postCompletionHandler: AuthenticationCompletionHandler? private let urlSession = URLSession(configuration: .default) /** Create instance of login manager to authenticate user and retreive access token. - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.shared.defaultAccessTokenIdentifier - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.shared.defaultKeychainAccessGroup - parameter loginType: The login type to use for logging in, defaults to Implicit - parameter productFlowPriority: An ordered list of the Uber apps to use for authentication, if available. - returns: An initialized LoginManager */ @objc public init(accessTokenIdentifier: String, keychainAccessGroup: String?, loginType: LoginType, productFlowPriority: [UberAuthenticationProductFlow]) { self.accessTokenIdentifier = accessTokenIdentifier self.keychainAccessGroup = keychainAccessGroup ?? Configuration.shared.defaultKeychainAccessGroup self.loginType = loginType self.productFlowPriority = productFlowPriority super.init() } /** Create instance of login manager to authenticate user and retreive access token. Authenticates using the main Uber rides product. - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.shared.defaultAccessTokenIdentifier - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.shared.defaultKeychainAccessGroup - parameter loginType: The login type to use for logging in, defaults to Implicit - returns: An initialized LoginManager */ @objc public convenience init(accessTokenIdentifier: String, keychainAccessGroup: String?, loginType: LoginType) { self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: keychainAccessGroup, loginType: loginType, productFlowPriority: [UberAuthenticationProductFlow(.rides)]) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Implicit Login Behavior - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.getDefaultKeychainAccessGroup() - parameter productFlowPriority: An ordered list of the Uber apps to use for authentication, if available. - returns: An initialized LoginManager */ @objc public convenience init(accessTokenIdentifier: String, keychainAccessGroup: String?, productFlowPriority: [UberAuthenticationProductFlow]) { self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: keychainAccessGroup, loginType: LoginType.implicit, productFlowPriority: productFlowPriority) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Implicit Login Behavior Authenticates using the main Uber rides product. - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() - parameter keychainAccessGroup: The keychain access group to use for saving the Access Token, defaults to Configuration.getDefaultKeychainAccessGroup() - returns: An initialized LoginManager */ @objc public convenience init(accessTokenIdentifier: String, keychainAccessGroup: String?) { self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: keychainAccessGroup, loginType: LoginType.implicit, productFlowPriority: [UberAuthenticationProductFlow(.rides)]) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Implicit Login Behavior & your Configuration's keychain access group - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() - parameter productFlowPriority: An ordered list of the Uber apps to use for authentication, if available. - returns: An initialized LoginManager */ @objc public convenience init(accessTokenIdentifier: String, productFlowPriority: [UberAuthenticationProductFlow]) { self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: nil, productFlowPriority: productFlowPriority) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Implicit Login Behavior & your Configuration's keychain access group Authenticates using the main Uber rides product. - parameter accessTokenIdentifier: The access token identifier to use for saving the Access Token, defaults to Configuration.getDefaultAccessTokenIdentifier() - returns: An initialized LoginManager */ @objc public convenience init(accessTokenIdentifier: String) { self.init(accessTokenIdentifier: accessTokenIdentifier, keychainAccessGroup: nil, productFlowPriority: [UberAuthenticationProductFlow(.rides)]) } /** Create instance of login manager to authenticate user and retreive access token. Uses the provided LoginType, with the accessTokenIdentifier & keychainAccessGroup defined in your Configuration - parameter loginType: The login behavior to use for logging in - parameter productFlowPriority: An ordered list of the Uber apps to use for authentication, if available. - returns: An initialized LoginManager */ @objc public convenience init(loginType: LoginType, productFlowPriority: [UberAuthenticationProductFlow]) { self.init(accessTokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, keychainAccessGroup: nil, loginType: loginType, productFlowPriority: productFlowPriority) } /** Create instance of login manager to authenticate user and retreive access token. Uses the provided LoginType, with the accessTokenIdentifier & keychainAccessGroup defined in your Configuration Authenticates using the main Uber rides product. - parameter loginType: The login behavior to use for logging in - returns: An initialized LoginManager */ @objc public convenience init(loginType: LoginType) { self.init(accessTokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, keychainAccessGroup: nil, loginType: loginType, productFlowPriority: [UberAuthenticationProductFlow(.rides)]) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Native LoginType, with the accessTokenIdentifier & keychainAccessGroup defined in your Configuration - parameter productFlowPriority: An ordered list of the Uber apps to use for authentication, if available. - returns: An initialized LoginManager */ @objc public convenience init(productFlowPriority: [UberAuthenticationProductFlow]) { self.init(accessTokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, keychainAccessGroup: nil, loginType: LoginType.native, productFlowPriority: productFlowPriority) } /** Create instance of login manager to authenticate user and retreive access token. Uses the Native LoginType, with the accessTokenIdentifier & keychainAccessGroup defined in your Configuration Authenticates using the main Uber rides product. - returns: An initialized LoginManager */ @objc public convenience override init() { self.init(accessTokenIdentifier: Configuration.shared.defaultAccessTokenIdentifier, keychainAccessGroup: nil, loginType: LoginType.native, productFlowPriority: [UberAuthenticationProductFlow(.rides)]) } // Mark: LoginManaging /** Launches view for user to log into Uber account and grant access to requested scopes. Access token (or error) is passed into completion handler. - parameter scopes: scopes being requested. - parameter presentingViewController: The presenting view controller present the login view controller over. - parameter prefillValues: Optional values to pre-populate the signin form with. - parameter completion: The LoginManagerRequestTokenHandler completion handler for login success/failure. */ @objc public func login(requestedScopes scopes: [UberScope], presentingViewController: UIViewController? = nil, prefillValues: Prefill? = nil, completion: AuthenticationCompletionHandler? = nil) { self.postCompletionHandler = completion UberAppDelegate.shared.loginManager = self loggingIn = true willEnterForegroundCalled = false let executeLogin: (String?) -> Void = { [weak self] requestUri in guard let self = self else { return } let authProvider = AuthenticationProvider(scopes: scopes, productFlowPriority: self.productFlowPriority, requestUri: requestUri) self.executeLogin(presentingViewController: presentingViewController, authenticationProvider: authProvider) } let responseType: OAuth.ResponseType? = { switch loginType { case .authorizationCode: return .code case .implicit: return .token case .native: return nil } }() if let prefillValues = prefillValues, let responseType = responseType { executeParRequest(prefillValues: prefillValues, responseType: responseType) { requestUri in executeLogin(requestUri) } } else { executeLogin(nil) } } /** Called via the RidesAppDelegate when the application is opened via a URL. Responsible for parsing the url and creating an OAuthToken. - parameter application: The UIApplication object. Pass in the value from the App Delegate - parameter url: The URL resource to open. As passed to the corresponding AppDelegate methods - parameter sourceApplication: The bundle ID of the app that is requesting your app to open the URL (url). As passed to the corresponding AppDelegate method (iOS 8) - parameter annotation: annotation: A property list object supplied by the source app to communicate information to the receiving app As passed to the corresponding AppDelegate method (iOS 8) - returns: true if the url was meant to be handled by the SDK, false otherwise */ public func application(_ application: UIApplication, open url: URL, sourceApplication: String?, annotation: Any?) -> Bool { if #available(iOS 13.0, *) { if loggingIn { authenticator?.consumeResponse(url: url, completion: loginCompletion) return true } else { return false } } guard let sourceApplication = sourceApplication else { return false } let sourceIsNative = loginType == .native && (sourceApplication.hasPrefix("com.ubercab") || sourceApplication.hasPrefix("com.ubereats")) let sourceIsSafariVC = loginType != .native && sourceApplication == "com.apple.SafariViewService" let sourceIsSafari = loginType != .native && sourceApplication == "com.apple.mobilesafari" let isValidSourceApplication = sourceIsNative || sourceIsSafariVC || sourceIsSafari if loggingIn && isValidSourceApplication { authenticator?.consumeResponse(url: url, completion: loginCompletion) return true } else { return false } } /** Called via the RidesAppDelegate when the application is opened via a URL. Responsible for parsing the url and creating an OAuthToken. (iOS 9+) - parameter application: The UIApplication object. Pass in the value from the App Delegate - parameter url: The URL resource to open. As passed to the corresponding AppDelegate methods - parameter options: A dictionary of URL handling options. As passed to the corresponding AppDelegate method. - returns: true if the url was meant to be handled by the SDK, false otherwise */ @available(iOS 9.0, *) public func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any]) -> Bool { let sourceApplication = options[UIApplication.OpenURLOptionsKey.sourceApplication] as? String let annotation = options[.annotation] as Any? return application(app, open: url, sourceApplication: sourceApplication, annotation: annotation) } /** Called via the RidesAppDelegate when the application is about to enter the foreground. Used to distinguish calls to applicationDidBecomeActive() that represent a true context switch vs. those that represent system dialogs appearing over the app */ public func applicationWillEnterForeground() { if loggingIn { willEnterForegroundCalled = true } } /** Called via the RidesAppDelegate when the application becomes active. Used to determine if a user abandons Native login without getting an access token. */ public func applicationDidBecomeActive() { if willEnterForegroundCalled && loggingIn && loginType == .native { self.handleLoginCanceled() UberAppDelegate.shared.loginManager = nil; } } // Mark: Private Interface private func executeParRequest(prefillValues: Prefill, responseType: OAuth.ResponseType, _ completion: @escaping (String?) -> Void) { let loginHint = prefillValues.dictValue guard !loginHint.isEmpty else { completion(nil) return } let request = Request( session: urlSession, endpoint: OAuth.par( clientID: Configuration.shared.clientID, loginHint: loginHint, responseType: responseType ) ) request?.prepare() request?.execute { response in let requestUri: String? = { guard let data = response.data, response.error == nil, let par = try? JSONDecoder.uberDecoder.decode(Par.self, from: data) else { return nil } return par.requestUri }() DispatchQueue.main.async { completion(requestUri) } } } private func executeLogin(presentingViewController: UIViewController?, authenticationProvider: AuthenticationProvider) { if let authenticator = authenticationProvider.authenticators(for: loginType).first, authenticator.authorizationURL.scheme == "https" { executeWebLogin(presentingViewController: presentingViewController, authenticator: authenticator) } else { executeDeeplinkLogin(presentingViewController: presentingViewController, authenticationProvider: authenticationProvider) } } // Delegates a web login to SFAuthenticationSession, SFSafariViewController, or just Safari private func executeWebLogin(presentingViewController: UIViewController?, authenticator: UberAuthenticating) { self.authenticator = authenticator executeSafariAuthLogin(authenticator: authenticator) } /// Login using SFAuthenticationSession private func executeSafariAuthLogin(authenticator: UberAuthenticating) { guard let bundleID = Bundle.main.bundleIdentifier else { preconditionFailure("You do not have a Bundle ID set for your app. You need a Bundle ID to use Uber Authentication") } let safariAuthenticationSession = SFAuthenticationSession(url: authenticator.authorizationURL, callbackURLScheme: bundleID, completionHandler: { (callbackURL, error) in if let callbackURL = callbackURL { authenticator.consumeResponse(url: callbackURL, completion: self.loginCompletion) } else { self.handleLoginCanceled() } self.safariAuthenticationSession = nil }) safariAuthenticationSession.start() self.safariAuthenticationSession = safariAuthenticationSession } /// Login using native deeplink private func executeDeeplinkLogin(presentingViewController: UIViewController?, authenticationProvider: AuthenticationProvider) { let authenticators = authenticationProvider.authenticators(for: .native) executeNativeAuthenticators(authenticators: authenticators) { (fallback: Bool) in if (!fallback) { return } // If we can't open the deeplink, fallback. // First check if the user requests auth code fallback regardless of scope. // Next, check for privileged scopes, which require auth code flow. // Since that requires server support, fallback to app store if not available. if Configuration.shared.alwaysUseAuthCodeFallback { self.loginType = .authorizationCode } else if authenticationProvider.scopes.contains(where: { $0.scopeType == .privileged }) { if (Configuration.shared.useFallback) { self.loginType = .authorizationCode } else { if let uberProductType = authenticationProvider.productFlowPriority.first?.uberProductType { let appStoreDeeplink: BaseDeeplink switch uberProductType { case .rides: appStoreDeeplink = RidesAppStoreDeeplink(userAgent: nil) case .eats: appStoreDeeplink = EatsAppStoreDeeplink(userAgent: nil) } appStoreDeeplink.execute(completion: { _ in self.loginCompletion(accessToken: nil, error: UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .unableToPresentLogin)) }) } return } } else { // Otherwise fallback to implicit flow self.loginType = .implicit } self.login(requestedScopes: authenticationProvider.scopes, presentingViewController: presentingViewController, completion: self.postCompletionHandler) } } private func executeNativeAuthenticators(authenticators: [UberAuthenticating], completion: @escaping ((_ fallback: Bool) -> Void)) { var fallback: Bool = false AsyncDispatcher.exec(for: authenticators.map({ return $0.authorizationURL }), with: { (url: URL) in self.authenticator = authenticators.first(where: { $0.authorizationURL == url }) }, asyncMethod: DeeplinkManager.shared.open(_:completion:), continue: { (error: NSError?) -> Bool in fallback = false if error == nil { // don't try next native authenticator return false } // If the user rejected the attempt to call the Uber app, don't use fallback. if self.loginType == .native, error?.code == DeeplinkErrorType.deeplinkNotFollowed.rawValue { self.loginCompletion(accessToken: nil, error: UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .unableToPresentLogin)) // don't try next native authenticator return false } else { fallback = true } // try next native authenticator return true }, finally: { completion(fallback) }) } func handleLoginCanceled() { loggingIn = false willEnterForegroundCalled = false loginCompletion(accessToken: nil, error: UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .userCancelled)) authenticator = nil } func loginCompletion(accessToken: AccessToken?, error: NSError?) { loggingIn = false willEnterForegroundCalled = false authenticator = nil oauthViewController?.dismiss(animated: true, completion: nil) var error = error if let accessToken = accessToken { let tokenIdentifier = accessTokenIdentifier let accessGroup = keychainAccessGroup let success = TokenManager.save(accessToken: accessToken, tokenIdentifier: tokenIdentifier, accessGroup: accessGroup) if !success { error = UberAuthenticationErrorFactory.errorForType(ridesAuthenticationErrorType: .unableToSaveAccessToken) print("Error: access token failed to save to keychain") } } postCompletionHandler?(accessToken, error) } }