Skip to content

Commit

Permalink
Merge pull request #294 from mohssenfathi/par
Browse files Browse the repository at this point in the history
Implements PAR request to prefill user information
  • Loading branch information
mohssenfathi authored Apr 2, 2024
2 parents b1a8832 + 120892b commit 2359902
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 51 deletions.
80 changes: 67 additions & 13 deletions Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,40 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {

private let shouldExchangeAuthCode: Bool

private let networkProvider: NetworkProviding

// MARK: Initializers

public init(presentationAnchor: ASPresentationAnchor = .init(),
scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes,
shouldExchangeAuthCode: Bool = true,
configurationProvider: ConfigurationProviding = DefaultConfigurationProvider(),
applicationLauncher: ApplicationLaunching = UIApplication.shared,
responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser()) {

shouldExchangeAuthCode: Bool = true) {
self.configurationProvider = DefaultConfigurationProvider()

guard let clientID: String = configurationProvider.clientID else {
preconditionFailure("No clientID specified in Info.plist")
}

guard let redirectURI: String = configurationProvider.redirectURI else {
preconditionFailure("No redirectURI specified in Info.plist")
}

self.applicationLauncher = UIApplication.shared
self.clientID = clientID
self.presentationAnchor = presentationAnchor
self.redirectURI = redirectURI
self.responseParser = AuthorizationCodeResponseParser()
self.shouldExchangeAuthCode = shouldExchangeAuthCode
self.networkProvider = NetworkProvider(baseUrl: Constants.baseUrl)
}

init(presentationAnchor: ASPresentationAnchor = .init(),
scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes,
shouldExchangeAuthCode: Bool = true,
configurationProvider: ConfigurationProviding = DefaultConfigurationProvider(),
applicationLauncher: ApplicationLaunching = UIApplication.shared,
responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser(),
networkProvider: NetworkProviding = NetworkProvider(baseUrl: Constants.baseUrl)) {

guard let clientID: String = configurationProvider.clientID else {
preconditionFailure("No clientID specified in Info.plist")
}
Expand All @@ -60,21 +85,25 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
self.redirectURI = redirectURI
self.responseParser = responseParser
self.shouldExchangeAuthCode = shouldExchangeAuthCode
self.networkProvider = networkProvider
}

// MARK: AuthProviding

public func execute(authDestination: AuthDestination,
prefill: Prefill?,
prefill: Prefill? = nil,
completion: @escaping Completion) {
self.completion = completion

// TODO: Implement PAR

executeLogin(
authDestination: authDestination,
requestURI: nil,
completion: completion
executePar(
prefill: prefill,
completion: { [weak self] requestURI in
self?.executeLogin(
authDestination: authDestination,
requestURI: requestURI,
completion: completion
)
}
)
}

Expand Down Expand Up @@ -200,7 +229,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
}

// If no native app was launched, fall back to in app login
self?.executeInAppLogin(
self?.executeInAppLogin(
requestURI: requestURI,
completion: completion
)
Expand Down Expand Up @@ -240,6 +269,31 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
}
)
}

private func executePar(prefill: Prefill?,
completion: @escaping (_ requestURI: String?) -> Void) {
guard let prefill else {
completion(nil)
return
}

let request = ParRequest(
clientID: clientID,
prefill: prefill.dictValue
)

networkProvider.execute(
request: request,
completion: { result in
switch result {
case .success(let response):
completion(response.requestURI)
case .failure:
completion(nil)
}
}
)
}

// MARK: Constants

Expand Down
10 changes: 5 additions & 5 deletions Sources/UberAuth/Authorize/AuthorizationCodeResponseParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@
import Foundation

/// @mockable
public protocol AuthorizationCodeResponseParsing {
protocol AuthorizationCodeResponseParsing {
func isValidResponse(url: URL, matching redirectURI: String) -> Bool
func callAsFunction(url: URL) -> Result<Client, UberAuthError>
}

///
/// A struct that validates and extracts values from a url containing an authorization code response
///
public struct AuthorizationCodeResponseParser: AuthorizationCodeResponseParsing {
struct AuthorizationCodeResponseParser: AuthorizationCodeResponseParsing {

public init() {}
init() {}

/// Determines whether the provided url corresponds to an authorization code response
/// by verifying that the url matches the expected redirect URI
Expand All @@ -25,7 +25,7 @@ public struct AuthorizationCodeResponseParser: AuthorizationCodeResponseParsing
/// - url: The url to parse
/// - redirectURI: The expected redirect url
/// - Returns: A boolean indicating whether or not the URLs match
public func isValidResponse(url: URL, matching redirectURI: String) -> Bool {
func isValidResponse(url: URL, matching redirectURI: String) -> Bool {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false),
let expectedComponents = URLComponents(string: redirectURI) else {
return false
Expand Down Expand Up @@ -53,7 +53,7 @@ public struct AuthorizationCodeResponseParser: AuthorizationCodeResponseParsing
///
/// - Parameter url: The url to parse
/// - Returns: A Result containing a client object built from the parsed values
public func callAsFunction(url: URL) -> Result<Client, UberAuthError> {
func callAsFunction(url: URL) -> Result<Client, UberAuthError> {
guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return .failure(.invalidResponse)
}
Expand Down
60 changes: 60 additions & 0 deletions Sources/UberAuth/Networking/NetworkProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//
// Copyright © Uber Technologies, Inc. All rights reserved.
//


import Foundation

protocol NetworkProviding {
func execute<R: Request>(request: R, completion: @escaping (Result<R.Response, UberAuthError>) -> ())
}

final class NetworkProvider: NetworkProviding {

private let baseUrl: String
private let session: URLSession
private let decoder = JSONDecoder()

init(baseUrl: String) {
self.baseUrl = baseUrl
self.session = URLSession(configuration: .default)
}

func execute<R: Request>(request: R, completion: @escaping (Result<R.Response, UberAuthError>) -> ()) {
guard let urlRequest = request.urlRequest(baseUrl: baseUrl) else {
completion(.failure(UberAuthError.invalidRequest("")))
return
}

let dataTask = session
.dataTask(
with: urlRequest,
completionHandler: { data, response, error in
if let error {
completion(.failure(.other(error)))
return
}

guard let data,
let response = response as? HTTPURLResponse else {
completion(.failure(UberAuthError.oAuth(.unsupportedResponseType)))
return
}

if let error = UberAuthError(response) {
completion(.failure(error))
return
}

do {
let decodedResponse = try self.decoder.decode(R.Response.self, from: data)
completion(.success(decodedResponse))
} catch {
completion(.failure(UberAuthError.serviceError))
}
}
)

dataTask.resume()
}
}
65 changes: 65 additions & 0 deletions Sources/UberAuth/PAR/ParRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
//
// Copyright © Uber Technologies, Inc. All rights reserved.
//


import Foundation

struct ParRequest: Request {

// MARK: Private Properties

private let clientID: String

private let prefill: [String: String]

// MARK: Initializers

init(clientID: String,
prefill: [String: String]) {
self.clientID = clientID
self.prefill = prefill
}

// MARK: Request

typealias Response = Par

var body: [String: String]? {
[
"client_id": clientID,
"response_type": "code",
"login_hint": loginHint
]
}

var method: HTTPMethod = .post

let path: String = "/oauth/v2/par"

let contentType: String = "application/x-www-form-urlencoded"

// MARK: Private

private var loginHint: String {
base64EncodedString(from: prefill) ?? ""
}

private func base64EncodedString(from dict: [String: String]) -> String? {
(try? JSONSerialization.data(withJSONObject: dict))?.base64EncodedString()
}
}

struct Par: Codable {

/// An identifier used for profile sharing
let requestURI: String?

/// Lifetime of the request_uri
let expiresIn: Date

enum CodingKeys: String, CodingKey {
case requestURI = "request_uri"
case expiresIn = "expires_in"
}
}
2 changes: 1 addition & 1 deletion Sources/UberAuth/Utilities/ApplicationLauncher.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import Foundation
import UIKit

/// @mockable
public protocol ApplicationLaunching {
protocol ApplicationLaunching {

func open(_ url: URL, options: [UIApplication.OpenExternalURLOptionsKey: Any], completionHandler: ((Bool) -> Void)?)
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/UberAuth/Utilities/ConfigurationProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,29 +7,29 @@ import Foundation
import UIKit

/// @mockable
public protocol ConfigurationProviding {
protocol ConfigurationProviding {
var clientID: String? { get }
var redirectURI: String? { get }

func isInstalled(app: UberApp, defaultIfUnregistered: Bool) -> Bool
}

public struct DefaultConfigurationProvider: ConfigurationProviding {
struct DefaultConfigurationProvider: ConfigurationProviding {

private let parser: PlistParser
private let contents: [String: Any]

public init() {
init() {
let parser = PlistParser(plistName: "Info")
self.parser = parser
self.contents = parser["UberAuth"] ?? [:]
}

public var clientID: String? {
var clientID: String? {
contents["ClientID"] as? String
}

public var redirectURI: String? {
var redirectURI: String? {
contents["RedirectURI"] as? String
}

Expand All @@ -42,7 +42,7 @@ public struct DefaultConfigurationProvider: ConfigurationProviding {
/// - app: The Uber application to check
/// - defaultIfUnregistered: The boolean value to return if the app's url scheme is not registered in the Info.plist
/// - Returns: A boolean indicating if the app is installed
public func isInstalled(app: UberApp, defaultIfUnregistered: Bool) -> Bool {
func isInstalled(app: UberApp, defaultIfUnregistered: Bool) -> Bool {
guard let registeredSchemes: [String] = parser["LSApplicationQueriesSchemes"],
registeredSchemes.contains(where: { $0 == app.deeplinkScheme }),
let url = URL(string: "\(app.deeplinkScheme)://") else {
Expand Down
2 changes: 1 addition & 1 deletion Sources/UberCore/Authentication/PrefillValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public class Prefill: Equatable {
self.lastName = lastName
}

var dictValue: [String: String] {
public var dictValue: [String: String] {
[
"email": email,
"phone": phoneNumber,
Expand Down
4 changes: 4 additions & 0 deletions examples/UberSDK/UberSDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */
B28217C82B97A2E400EE786D /* AuthManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28217C72B97A2E400EE786D /* AuthManagerTests.swift */; };
B28A41712BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */; };
B28A41752BAE331300F727C7 /* ParRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28A41742BAE331300F727C7 /* ParRequestTests.swift */; };
B28CDD322BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD312BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift */; };
B28CDD342BA4BE2100EB1BBD /* RequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD332BA4BE2100EB1BBD /* RequestTests.swift */; };
B28CDD362BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B28CDD352BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift */; };
Expand Down Expand Up @@ -48,6 +49,7 @@
/* Begin PBXFileReference section */
B28217C72B97A2E400EE786D /* AuthManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthManagerTests.swift; sourceTree = "<group>"; };
B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeResponseParserTests.swift; sourceTree = "<group>"; };
B28A41742BAE331300F727C7 /* ParRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParRequestTests.swift; sourceTree = "<group>"; };
B28CDD312BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationCodeAuthProviderTests.swift; sourceTree = "<group>"; };
B28CDD332BA4BE2100EB1BBD /* RequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestTests.swift; sourceTree = "<group>"; };
B28CDD352BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizeRequestTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -102,6 +104,7 @@
B28217C72B97A2E400EE786D /* AuthManagerTests.swift */,
B28CDD312BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift */,
B28CDD352BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift */,
B28A41742BAE331300F727C7 /* ParRequestTests.swift */,
B28A41702BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift */,
B28CDD332BA4BE2100EB1BBD /* RequestTests.swift */,
B2D096942B97C4A00093B510 /* UberAuthErrorTests.swift */,
Expand Down Expand Up @@ -333,6 +336,7 @@
B28217C82B97A2E400EE786D /* AuthManagerTests.swift in Sources */,
B28CDD362BA4C6CA00EB1BBD /* AuthorizeRequestTests.swift in Sources */,
B2D096932B97B8E70093B510 /* UberAuthMocks.swift in Sources */,
B28A41752BAE331300F727C7 /* ParRequestTests.swift in Sources */,
B28CDD322BA403A900EB1BBD /* AuthorizationCodeAuthProviderTests.swift in Sources */,
B28A41712BACA97000F727C7 /* AuthorizationCodeResponseParserTests.swift in Sources */,
);
Expand Down
Loading

0 comments on commit 2359902

Please sign in to comment.