Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement prompt for re-login/consent #308

Merged
merged 1 commit into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Sources/UberAuth/AuthProviding.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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,
Expand All @@ -104,6 +109,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
self.networkProvider = networkProvider
self.tokenManager = tokenManager
self.scopes = scopes
self.prompt = prompt
}

// MARK: AuthProviding
Expand Down Expand Up @@ -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
Expand Down
4 changes: 4 additions & 0 deletions Sources/UberAuth/Authorize/AuthorizeRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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
Expand All @@ -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: " ")
Expand Down
38 changes: 38 additions & 0 deletions Sources/UberAuth/Authorize/Prompt.swift
Original file line number Diff line number Diff line change
@@ -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: " ")
}
}
73 changes: 36 additions & 37 deletions examples/UberSDK/UberSDK/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -263,15 +243,34 @@ struct ContentView: View {
label: {
HStack(spacing: 0) {
if let item { Text(item.rawValue) }
Spacer()
content()
.frame(maxWidth: .infinity, alignment: .trailing)
if showDisclosureIndicator { emptyNavigationLink }
}
}
)
.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<Bool>) -> 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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)
Expand All @@ -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() {
Expand Down
Loading