From cf70a7014f212c0557daf60e3e9329ff6a5eb0ee Mon Sep 17 00:00:00 2001 From: Mohammad Fathi Date: Tue, 2 Jul 2024 09:14:57 -0700 Subject: [PATCH] Implement prompt for re-login/consent --- Sources/UberAuth/AuthProviding.swift | 6 +- .../AuthorizationCodeAuthProvider.swift | 9 ++- .../UberAuth/Authorize/AuthorizeRequest.swift | 4 + Sources/UberAuth/Authorize/Prompt.swift | 38 ++++++++++ examples/UberSDK/UberSDK/ContentView.swift | 73 +++++++++---------- .../AuthorizationCodeAuthProviderTests.swift | 37 ++++++++++ .../UberAuth/AuthorizeRequestTests.swift | 5 ++ 7 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 Sources/UberAuth/Authorize/Prompt.swift diff --git a/Sources/UberAuth/AuthProviding.swift b/Sources/UberAuth/AuthProviding.swift index 118e0e0..268c97e 100644 --- a/Sources/UberAuth/AuthProviding.swift +++ b/Sources/UberAuth/AuthProviding.swift @@ -20,11 +20,13 @@ extension AuthProviding where Self == AuthorizationCodeAuthProvider { public static func authorizationCode(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, - shouldExchangeAuthCode: Bool = true) -> Self { + shouldExchangeAuthCode: Bool = true, + prompt: Prompt? = nil) -> Self { AuthorizationCodeAuthProvider( presentationAnchor: presentationAnchor, scopes: scopes, - shouldExchangeAuthCode: shouldExchangeAuthCode + shouldExchangeAuthCode: shouldExchangeAuthCode, + prompt: prompt ) } } diff --git a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift index 141c3c1..d4daf1d 100644 --- a/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift +++ b/Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift @@ -48,11 +48,14 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { private let scopes: [String] + private let prompt: Prompt? + // MARK: Initializers public init(presentationAnchor: ASPresentationAnchor = .init(), scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, - shouldExchangeAuthCode: Bool = false) { + shouldExchangeAuthCode: Bool = false, + prompt: Prompt? = nil) { self.configurationProvider = DefaultConfigurationProvider() guard let clientID: String = configurationProvider.clientID else { @@ -73,11 +76,13 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.networkProvider = NetworkProvider(baseUrl: Constants.baseUrl) self.tokenManager = TokenManager() self.scopes = scopes + self.prompt = prompt } init(presentationAnchor: ASPresentationAnchor = .init(), authenticationSessionBuilder: AuthenticationSessionBuilder? = nil, scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes, + prompt: Prompt? = nil, shouldExchangeAuthCode: Bool = false, configurationProvider: ConfigurationProviding = DefaultConfigurationProvider(), applicationLauncher: ApplicationLaunching = UIApplication.shared, @@ -104,6 +109,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { self.networkProvider = networkProvider self.tokenManager = tokenManager self.scopes = scopes + self.prompt = prompt } // MARK: AuthProviding @@ -195,6 +201,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding { app: nil, clientID: clientID, codeChallenge: shouldExchangeAuthCode ? pkce.codeChallenge : nil, + prompt: prompt, redirectURI: redirectURI, requestURI: requestURI, scopes: scopes diff --git a/Sources/UberAuth/Authorize/AuthorizeRequest.swift b/Sources/UberAuth/Authorize/AuthorizeRequest.swift index 0376db4..6623832 100644 --- a/Sources/UberAuth/Authorize/AuthorizeRequest.swift +++ b/Sources/UberAuth/Authorize/AuthorizeRequest.swift @@ -17,6 +17,7 @@ struct AuthorizeRequest: NetworkRequest { private let app: UberApp? private let codeChallenge: String? private let clientID: String + private let prompt: Prompt? private let redirectURI: String private let requestURI: String? private let scopes: [String] @@ -26,12 +27,14 @@ struct AuthorizeRequest: NetworkRequest { init(app: UberApp?, clientID: String, codeChallenge: String?, + prompt: Prompt? = nil, redirectURI: String, requestURI: String?, scopes: [String] = []) { self.app = app self.clientID = clientID self.codeChallenge = codeChallenge + self.prompt = prompt self.redirectURI = redirectURI self.requestURI = requestURI self.scopes = scopes @@ -47,6 +50,7 @@ struct AuthorizeRequest: NetworkRequest { "client_id": clientID, "code_challenge": codeChallenge, "code_challenge_method": codeChallenge != nil ? "S256" : nil, + "prompt": prompt?.stringValue, "redirect_uri": redirectURI, "request_uri": requestURI, "scope": scopes.joined(separator: " ") diff --git a/Sources/UberAuth/Authorize/Prompt.swift b/Sources/UberAuth/Authorize/Prompt.swift new file mode 100644 index 0000000..b380b92 --- /dev/null +++ b/Sources/UberAuth/Authorize/Prompt.swift @@ -0,0 +1,38 @@ +// +// Copyright © Uber Technologies, Inc. All rights reserved. +// + + +import Foundation + +/// +/// A type defining values that specify whether the Authorization Server prompts the End-User for reauthentication and consent. +/// Current supported values are `login` and `consent` + +/// See OpedID standards for more information. +/// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest +/// +public struct Prompt: OptionSet { + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = rawValue + } + + /// The Authorization Server SHOULD prompt the End-User for reauthentication. + /// If it cannot reauthenticate the End-User, it MUST return an error, typically `login_required`. + public static let login = Prompt(rawValue: 1 << 0) + + /// The Authorization Server SHOULD prompt the End-User for consent before returning information to the Client. + /// If it cannot obtain consent, it MUST return an error, typically `consent_required`. + public static let consent = Prompt(rawValue: 1 << 1) + + /// Creates a space seperated string containing the values in the option set + var stringValue: String { + var values: [String] = [] + if contains(.login) { values.append("login") } + if contains(.consent) { values.append("consent") } + return values.joined(separator: " ") + } +} diff --git a/examples/UberSDK/UberSDK/ContentView.swift b/examples/UberSDK/UberSDK/ContentView.swift index dfc88a1..b091041 100644 --- a/examples/UberSDK/UberSDK/ContentView.swift +++ b/examples/UberSDK/UberSDK/ContentView.swift @@ -29,14 +29,21 @@ final class Content { var type: LoginType? = .authorizationCode var destination: LoginDestination? = .inApp var isTokenExchangeEnabled: Bool = true + var shouldForceLogin: Bool = false + var shouldForceConsent: Bool = false var isPrefillExpanded: Bool = false var response: String? var prefillBuilder = PrefillBuilder() func login() { + var promt: Prompt = [] + if shouldForceLogin { promt.insert(.login) } + if shouldForceConsent { promt.insert(.consent) } + let authProvider: AuthProviding = .authorizationCode( - shouldExchangeAuthCode: isTokenExchangeEnabled + shouldExchangeAuthCode: isTokenExchangeEnabled, + prompt: promt ) let authDestination: AuthDestination = { @@ -72,6 +79,8 @@ final class Content { case type = "Auth Type" case destination = "Destination" case tokenExchange = "Exchange Auth Code for Token" + case forceLogin = "Always ask for Login" + case forceConsent = "Always ask for Consent" case prefill = "Prefill Values" case firstName = "First Name" case lastName = "Last Name" @@ -169,41 +178,12 @@ struct ContentView: View { @ViewBuilder private var loginSection: some View { - row( - item: .type, - content: { - Text(content.type?.description ?? "") - .foregroundStyle(.gray) - }, - tapHandler: { content.selection = .type } - ) - - row( - item: .destination, - content: { - Text(content.destination?.description ?? "") - .foregroundStyle(.gray) - }, - tapHandler: { content.selection = .destination } - ) - - row( - item: .tokenExchange, - content: { - Toggle(isOn: $content.isTokenExchangeEnabled, label: { EmptyView() }) - }, - showDisclosureIndicator: false, - tapHandler: nil - ) - - row( - item: .prefill, - content: { - Toggle(isOn: $content.isPrefillExpanded, label: { EmptyView() }) - }, - showDisclosureIndicator: false, - tapHandler: nil - ) + textRow(.type, value: content.type?.description) + textRow(.destination, value: content.destination?.description) + toggleRow(.tokenExchange, value: $content.isTokenExchangeEnabled) + toggleRow(.forceLogin, value: $content.shouldForceLogin) + toggleRow(.forceConsent, value: $content.shouldForceConsent) + toggleRow(.prefill, value: $content.isPrefillExpanded) if content.isPrefillExpanded { row( @@ -263,8 +243,8 @@ struct ContentView: View { label: { HStack(spacing: 0) { if let item { Text(item.rawValue) } - Spacer() content() + .frame(maxWidth: .infinity, alignment: .trailing) if showDisclosureIndicator { emptyNavigationLink } } } @@ -272,6 +252,25 @@ struct ContentView: View { .tint(.black) } + private func textRow(_ item: Content.Item, value: String?) -> some View { + row( + item: item, + content: { Text(value ?? "").foregroundStyle(.gray) }, + tapHandler: { content.selection = item } + ) + } + + private func toggleRow(_ item: Content.Item, value: Binding) -> some View { + row( + item: item, + content: { + Toggle(isOn: value, label: { EmptyView() }) + }, + showDisclosureIndicator: false, + tapHandler: nil + ) + } + private let emptyNavigationLink: some View = NavigationLink.empty .frame(width: 17, height: 0) .frame(alignment: .leading) diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift index c4aebbf..be72cc1 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizationCodeAuthProviderTests.swift @@ -86,6 +86,43 @@ final class AuthorizationCodeAuthProviderTests: XCTestCase { XCTAssertTrue(hasCalledAuthenticationSessionBuilder) } + + func test_executeInAppLogin_prompt_includedInAuthorizeRequest() { + + configurationProvider.isInstalledHandler = { _, _ in + true + } + + let applicationLauncher = ApplicationLaunchingMock() + applicationLauncher.openHandler = { _, _, completion in + completion?(true) + } + + var hasCalledAuthenticationSessionBuilder: Bool = false + let prompt: Prompt = [.login, .consent] + let promptString = prompt.stringValue.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed)! + + let authenticationSessionBuilder: AuthorizationCodeAuthProvider.AuthenticationSessionBuilder = { _, _, url, _ in + XCTAssertTrue(url.query()!.contains("prompt=\(promptString)")) + hasCalledAuthenticationSessionBuilder = true + return AuthenticationSessioningMock() + } + + let provider = AuthorizationCodeAuthProvider( + authenticationSessionBuilder: authenticationSessionBuilder, + prompt: [.login, .consent], + shouldExchangeAuthCode: false, + configurationProvider: configurationProvider, + applicationLauncher: applicationLauncher + ) + + provider.execute( + authDestination: .inApp, + completion: { result in } + ) + + XCTAssertTrue(hasCalledAuthenticationSessionBuilder) + } func test_execute_existingSession_returnsExistingAuthSessionError() { let provider = AuthorizationCodeAuthProvider( diff --git a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizeRequestTests.swift b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizeRequestTests.swift index 52bf427..3ba30d1 100644 --- a/examples/UberSDK/UberSDKTests/UberAuth/AuthorizeRequestTests.swift +++ b/examples/UberSDK/UberSDKTests/UberAuth/AuthorizeRequestTests.swift @@ -10,10 +10,14 @@ final class AuthorizeRequestTests: XCTestCase { func test_generatedUrl() { + let prompt: Prompt = [.consent, .login] + let promptString = prompt.stringValue.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed)! + let request = AuthorizeRequest( app: nil, clientID: "test_client_id", codeChallenge: "code_challenge", + prompt: prompt, redirectURI: "redirect_uri", requestURI: "request_url" ) @@ -30,6 +34,7 @@ final class AuthorizeRequestTests: XCTestCase { XCTAssertTrue(url.query()!.contains("request_uri=request_url")) XCTAssertTrue(url.query()!.contains("code_challenge_method=S256")) XCTAssertTrue(url.query()!.contains("redirect_uri=redirect_uri")) + XCTAssertTrue(url.query()!.contains("prompt=\(promptString)")) } func test_appSpecific_generatedUrls() {