diff --git a/Sources/UberAuth/AuthProviding.swift b/Sources/UberAuth/AuthProviding.swift index 6290acc..20c46cb 100644 --- a/Sources/UberAuth/AuthProviding.swift +++ b/Sources/UberAuth/AuthProviding.swift @@ -49,12 +49,14 @@ extension AuthProviding where Self == AuthorizationCodeAuthProvider { public static func authorizationCode(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, shouldExchangeAuthCode: Bool = true, - prompt: Prompt? = nil) -> Self { + prompt: Prompt? = nil, + environment: UberEnvironment = .production) -> Self { AuthorizationCodeAuthProvider( presentationAnchor: presentationAnchor, scopes: scopes, shouldExchangeAuthCode: shouldExchangeAuthCode, - prompt: prompt + prompt: prompt, + environment: environment ) } } diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index d3e9d9e..4724b65 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -67,15 +67,18 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { private let tokenManager: TokenManaging private let scopes: [String] - + private let prompt: Prompt? - + + private let baseUrl: String + // MARK: Initializers - + public init(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, shouldExchangeAuthCode: Bool = false, - prompt: Prompt? = nil) { + prompt: Prompt? = nil, + environment: UberEnvironment = .production) { self.configurationProvider = ConfigurationProvider() self.applicationLauncher = UIApplication.shared self.authenticationSessionBuilder = nil @@ -84,12 +87,13 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.redirectURI = configurationProvider.redirectURI self.responseParser = AuthorizationCodeResponseParser() self.shouldExchangeAuthCode = shouldExchangeAuthCode - self.networkProvider = NetworkProvider(baseUrl: Constants.baseUrl) - self.tokenManager = TokenManager() + self.baseUrl = environment.baseUrl + "/v2" + self.networkProvider = NetworkProvider(baseUrl: self.baseUrl) + self.tokenManager = TokenManager(environment: environment) self.scopes = scopes self.prompt = prompt } - + init(presentationAnchor: ASPresentationAnchor = .init(), authenticationSessionBuilder: AuthenticationSessionBuilder? = nil, scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, @@ -98,9 +102,10 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { configurationProvider: ConfigurationProviding = ConfigurationProvider(), applicationLauncher: ApplicationLaunching = UIApplication.shared, responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser(), - networkProvider: NetworkProviding = NetworkProvider(baseUrl: Constants.baseUrl), - tokenManager: TokenManaging = TokenManager()) { - + networkProvider: NetworkProviding = NetworkProvider(baseUrl: UberEnvironment.production.baseUrl + "/v2"), + tokenManager: TokenManaging = TokenManager(), + environment: UberEnvironment = .production) { + self.applicationLauncher = applicationLauncher self.authenticationSessionBuilder = authenticationSessionBuilder self.clientID = configurationProvider.clientID @@ -109,6 +114,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.redirectURI = configurationProvider.redirectURI self.responseParser = responseParser self.shouldExchangeAuthCode = shouldExchangeAuthCode + self.baseUrl = environment.baseUrl + "/v2" self.networkProvider = networkProvider self.tokenManager = tokenManager self.scopes = scopes @@ -244,11 +250,11 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { + guard let url = request.url(baseUrl: baseUrl) else { completion(.failure(.invalidRequest("Invalid base URL"))) return } - + guard let callbackURL = URL(string: redirectURI), let callbackURLScheme = callbackURL.scheme else { completion(.failure(.invalidRequest("Invalid redirect URI"))) @@ -284,7 +290,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { + guard let url = request.url(baseUrl: baseUrl) else { throw UberAuthError.invalidRequest("Invalid base URL") } @@ -415,11 +421,11 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { + guard let url = request.url(baseUrl: baseUrl) else { completion?(false) return } - + DispatchQueue.main.async { self.applicationLauncher.launch( url, @@ -448,7 +454,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { scopes: scopes ) - guard let url = request.url(baseUrl: Constants.baseUrl) else { return false } + guard let url = request.url(baseUrl: baseUrl) else { return false } return await applicationLauncher.launch(url) } @@ -545,13 +551,6 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { return client } - // MARK: Constants - - private enum Constants { - static let clientIDKey = "ClientID" - static let redirectURI = "RedirectURI" - static let baseUrl = "https://auth.uber.com/v2" - } } diff --git a/Sources/UberAuth/Token/TokenManager.swift b/Sources/UberAuth/Token/TokenManager.swift index 81772e7..b92e52f 100644 --- a/Sources/UberAuth/Token/TokenManager.swift +++ b/Sources/UberAuth/Token/TokenManager.swift @@ -24,6 +24,7 @@ import Foundation +import UberCore /// @mockable public protocol TokenManaging { @@ -66,15 +67,19 @@ public extension TokenManaging { } public final class TokenManager: TokenManaging { - + public static let defaultAccessTokenIdentifier: String = "UberAccessTokenKey" - + public static let defaultKeychainAccessGroup: String = "" - + private let keychainUtility: KeychainUtilityProtocol - - public init(keychainUtility: KeychainUtilityProtocol = KeychainUtility()) { + + private let regionHost: String + + public init(keychainUtility: KeychainUtilityProtocol = KeychainUtility(), + environment: UberEnvironment = .production) { self.keychainUtility = keychainUtility + self.regionHost = environment.baseUrl } // MARK: Save @@ -128,9 +133,9 @@ public final class TokenManager: TokenManaging { // MARK: Private Interface - /// Removes all cookies in the shared cookie store corresponding with the auth.uber.com domain + /// Removes all cookies in the shared cookie store corresponding with the auth domain private func deleteCookies() { - guard let loginUrl = URL(string: Constants.regionHost) else { + guard let loginUrl = URL(string: regionHost) else { return } @@ -142,10 +147,4 @@ public final class TokenManager: TokenManaging { } } } - - // MARK: Constants - - private enum Constants { - static let regionHost = "https://auth.uber.com" - } } diff --git a/Sources/UberCore/UberEnvironment.swift b/Sources/UberCore/UberEnvironment.swift new file mode 100644 index 0000000..aea33a3 --- /dev/null +++ b/Sources/UberCore/UberEnvironment.swift @@ -0,0 +1,37 @@ +// +// UberEnvironment.swift +// UberCore +// +// Copyright © 2024 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 Foundation + +public enum UberEnvironment { + case production + case sandbox + + public var baseUrl: String { + switch self { + case .production: return "https://auth.uber.com" + case .sandbox: return "https://sandbox-login.uber.com" + } + } +} diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index 4de0bef..803413e 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -48,6 +48,7 @@ final class Content { var selection: Item? var type: LoginType? = .authorizationCode var destination: LoginDestination? = .inApp + var environment: LoginEnvironment? = .production var isTokenExchangeEnabled: Bool = true var shouldForceLogin: Bool = false var shouldForceConsent: Bool = false @@ -75,10 +76,11 @@ final class Content { var prompt: Prompt = [] if shouldForceLogin { prompt.insert(.login) } if shouldForceConsent { prompt.insert(.consent) } - + let authProvider: AuthProviding = .authorizationCode( shouldExchangeAuthCode: isTokenExchangeEnabled, - prompt: prompt + prompt: prompt, + environment: environment == .sandbox ? .sandbox : .production ) let authDestination: AuthDestination = { @@ -116,6 +118,7 @@ final class Content { enum Item: String, Hashable, Identifiable { case type = "Auth Type" case destination = "Destination" + case environment = "Environment" case tokenExchange = "Exchange Auth Code for Token" case forceLogin = "Always ask for Login" case forceConsent = "Always ask for Consent" @@ -166,6 +169,12 @@ struct ContentView: View { options: LoginDestination.allCases ) .presentationDetents([.height(200)]) + case .environment: + SelectionView( + selection: $content.environment, + options: LoginEnvironment.allCases + ) + .presentationDetents([.height(200)]) default: EmptyView() } @@ -201,6 +210,7 @@ struct ContentView: View { textRow(.type, value: content.type?.description) textRow(.destination, value: content.destination?.description) + textRow(.environment, value: content.environment?.description) toggleRow(.tokenExchange, value: $content.isTokenExchangeEnabled) toggleRow(.forceLogin, value: $content.shouldForceLogin) toggleRow(.forceConsent, value: $content.shouldForceConsent) diff --git a/examples/UberSDK/UberSDK/Info.plist b/examples/UberSDK/UberSDK/Info.plist index 0d0e4e4..c3dc43b 100644 --- a/examples/UberSDK/UberSDK/Info.plist +++ b/examples/UberSDK/UberSDK/Info.plist @@ -24,9 +24,9 @@ Uber ClientID - [Client ID] + u6fWe9U2aQv1hE-dPXhzPXmnmOD45n6N RedirectURI - com.uber.UberSDK://oauth/consumer + https://www.uber.com/ DisplayName [App Name] diff --git a/examples/UberSDK/UberSDK/SelectionOptions.swift b/examples/UberSDK/UberSDK/SelectionOptions.swift index f415100..58aa06e 100644 --- a/examples/UberSDK/UberSDK/SelectionOptions.swift +++ b/examples/UberSDK/UberSDK/SelectionOptions.swift @@ -35,8 +35,16 @@ enum LoginType: String, CaseIterable, SelectionOption { enum LoginDestination: String, CaseIterable, SelectionOption { case inApp = "In App" - case native = "Native" - + case native = "Native" + + var description: String { rawValue } + var id: String { rawValue } +} + +enum LoginEnvironment: String, CaseIterable, SelectionOption { + case production = "Production" + case sandbox = "Sandbox" + var description: String { rawValue } var id: String { rawValue } } diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift index a9b7a88..c611d75 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift @@ -1025,14 +1025,14 @@ extension AuthorizationCodeAuthProviderTests { XCTAssertNotNil(error as? UberAuthError) } } - + func test_execute_async_existingSession_throwsError() async { let provider = AuthorizationCodeAuthProvider( shouldExchangeAuthCode: false, configurationProvider: configurationProvider ) provider.currentSession = AuthenticationSessioningMock() - + do { _ = try await provider.execute(authDestination: .inApp, prefill: nil) XCTFail("Should have thrown error") @@ -1040,4 +1040,66 @@ extension AuthorizationCodeAuthProviderTests { XCTAssertNotNil(error as? UberAuthError) } } + + // MARK: Environment + + func test_environment_production_usesProductionBaseUrl() { + var capturedUrl: URL? + let authenticationSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, url, _ in + capturedUrl = url + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authenticationSessionBuilder, + configurationProvider: configurationProvider, + environment: .production + ) + + provider.execute(authDestination: .inApp, completion: { _ in }) + + XCTAssertTrue(capturedUrl?.absoluteString.contains("auth.uber.com") == true) + XCTAssertFalse(capturedUrl?.absoluteString.contains("sandbox-login.uber.com") == true) + } + + func test_environment_sandbox_usesSandboxBaseUrl() { + var capturedUrl: URL? + let authenticationSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, url, _ in + capturedUrl = url + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authenticationSessionBuilder, + configurationProvider: configurationProvider, + environment: .sandbox + ) + + provider.execute(authDestination: .inApp, completion: { _ in }) + + XCTAssertTrue(capturedUrl?.absoluteString.contains("sandbox-login.uber.com") == true) + XCTAssertFalse(capturedUrl?.absoluteString.contains("auth.uber.com") == true) + } + + func test_environment_sandbox_nativeLogin_usesSandboxBaseUrl() { + configurationProvider.isInstalledHandler = { _, _ in true } + + let expectation = XCTestExpectation() + let applicationLauncher = ApplicationLaunchingMock() + applicationLauncher.launchHandler = { url, completion in + XCTAssertTrue(url.absoluteString.contains("sandbox-login.uber.com")) + expectation.fulfill() + completion?(true) + } + + let provider = AuthorizationCodeAuthProvider( + configurationProvider: configurationProvider, + applicationLauncher: applicationLauncher, + environment: .sandbox + ) + + provider.execute(authDestination: .native(appPriority: [.rides]), completion: { _ in }) + + wait(for: [expectation], timeout: 0.2) + } }