Skip to content

Commit

Permalink
Merge pull request #295 from mohssenfathi/token-exchange
Browse files Browse the repository at this point in the history
Implement Token exchange request
  • Loading branch information
mohssenfathi authored Apr 5, 2024
2 parents 2359902 + 0fe1a31 commit 97bbae3
Show file tree
Hide file tree
Showing 13 changed files with 442 additions and 131 deletions.
97 changes: 82 additions & 15 deletions Sources/UberAuth/Authorize/AuthorizationCodeAuthProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {

public init(presentationAnchor: ASPresentationAnchor = .init(),
scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes,
shouldExchangeAuthCode: Bool = true) {
shouldExchangeAuthCode: Bool = false) {
self.configurationProvider = DefaultConfigurationProvider()

guard let clientID: String = configurationProvider.clientID else {
Expand All @@ -64,7 +64,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {

init(presentationAnchor: ASPresentationAnchor = .init(),
scopes: [String] = AuthorizationCodeAuthProvider.defaultScopes,
shouldExchangeAuthCode: Bool = true,
shouldExchangeAuthCode: Bool = false,
configurationProvider: ConfigurationProviding = DefaultConfigurationProvider(),
applicationLauncher: ApplicationLaunching = UIApplication.shared,
responseParser: AuthorizationCodeResponseParsing = AuthorizationCodeResponseParser(),
Expand Down Expand Up @@ -93,25 +93,51 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
public func execute(authDestination: AuthDestination,
prefill: Prefill? = nil,
completion: @escaping Completion) {
self.completion = completion

// Completion is stored for native handle callback
// Upon completion, intercept result and exchange for token if enabled
let authCompletion: Completion = { [weak self] result in
guard let self else { return }

switch result {
case .success(let client):
// Exchange auth code for token if needed
if shouldExchangeAuthCode,
let code = client.authorizationCode {
exchange(code: code, completion: completion)
self.completion = nil
return
}
case .failure:
break
}

completion(result)
self.completion = nil
}

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

self.completion = authCompletion
}

public func handle(response url: URL) -> Bool {
guard responseParser.isValidResponse(url: url, matching: redirectURI) else {
return false
}
completion?(responseParser(url: url))

let result = responseParser(url: url)
completion?(result)

return true
}

Expand Down Expand Up @@ -170,16 +196,10 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
anchor: presentationAnchor,
callbackURLScheme: callbackURLScheme,
url: url,
completion: { result in
if self.shouldExchangeAuthCode,
case .success(let client) = result,
let code = client.authorizationCode {
// TODO: Exchange auth code here
self.currentSession = nil
return
}
completion: { [weak self] result in
guard let self else { return }
completion(result)
self.currentSession = nil
currentSession = nil
}
)

Expand Down Expand Up @@ -237,6 +257,12 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
)
}

/// Attempts to launch a native app with an SSO universal link.
/// Calls a closure with a boolean indicating if the application was successfully opened.
///
/// - Parameters:
/// - context: A tuple of the destination app and an optional requestURI
/// - completion: An optional closure indicating whether or not the app was launched
private func launch(context: (app: UberApp, requestURI: String?),
completion: ((Bool) -> Void)?) {
let (app, requestURI) = context
Expand Down Expand Up @@ -271,7 +297,7 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
}

private func executePar(prefill: Prefill?,
completion: @escaping (_ requestURI: String?) -> Void) {
completion: @escaping (_ requestURI: String?) -> Void) {
guard let prefill else {
completion(nil)
return
Expand All @@ -295,6 +321,33 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
)
}

// MARK: Token Exchange

/// Makes a request to the /token endpoing to exchange the authorization code
/// for an access token.
/// - Parameter code: The authorization code to exchange
private func exchange(code: String, completion: @escaping Completion) {
let request = TokenRequest(
clientID: clientID,
authorizationCode: code,
redirectURI: redirectURI,
codeVerifier: pkce.codeVerifier
)

networkProvider.execute(
request: request,
completion: { [weak self] result in
switch result {
case .success(let response):
let client = Client(tokenResponse: response)
completion(.success(client))
case .failure(let error):
completion(.failure(error))
}
}
)
}

// MARK: Constants

private enum Constants {
Expand All @@ -303,3 +356,17 @@ public final class AuthorizationCodeAuthProvider: AuthProviding {
static let baseUrl = "https://auth.uber.com/v2"
}
}


fileprivate extension Client {

init(tokenResponse: TokenRequest.Response) {
self = .init(
accessToken: tokenResponse.accessToken,
refreshToken: tokenResponse.refreshToken,
tokenType: tokenResponse.tokenType,
expiresIn: tokenResponse.expiresIn,
scope: tokenResponse.scope
)
}
}
7 changes: 6 additions & 1 deletion Sources/UberAuth/Authorize/AuthorizeRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@

import Foundation

struct AuthorizeRequest: Request {
///
/// Defines a network request conforming to the OAuth 2.0 standard authorization request
/// for the authorization code grant flow.
/// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2
///
struct AuthorizeRequest: NetworkRequest {

// MARK: Private Properties

Expand Down
5 changes: 3 additions & 2 deletions Sources/UberAuth/Networking/NetworkProvider.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@

import Foundation

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

final class NetworkProvider: NetworkProviding {
Expand All @@ -20,7 +21,7 @@ final class NetworkProvider: NetworkProviding {
self.session = URLSession(configuration: .default)
}

func execute<R: Request>(request: R, completion: @escaping (Result<R.Response, UberAuthError>) -> ()) {
func execute<R: NetworkRequest>(request: R, completion: @escaping (Result<R.Response, UberAuthError>) -> ()) {
guard let urlRequest = request.urlRequest(baseUrl: baseUrl) else {
completion(.failure(UberAuthError.invalidRequest("")))
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import Foundation

protocol Request {
protocol NetworkRequest {

associatedtype Response: Codable

var body: [String: String]? { get }
Expand All @@ -19,7 +19,7 @@ protocol Request {
var scheme: String? { get }
}

extension Request {
extension NetworkRequest {

var contentType: String? { nil }
var body: [String: String]? { nil }
Expand All @@ -30,7 +30,7 @@ extension Request {
var scheme: String? { nil }
}

extension Request {
extension NetworkRequest {

func url(baseUrl: String) -> URL? {
urlRequest(baseUrl: baseUrl)?.url
Expand Down
2 changes: 1 addition & 1 deletion Sources/UberAuth/PAR/ParRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import Foundation

struct ParRequest: Request {
struct ParRequest: NetworkRequest {

// MARK: Private Properties

Expand Down
101 changes: 101 additions & 0 deletions Sources/UberAuth/Token/TokenRequest.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
//
// Copyright © Uber Technologies, Inc. All rights reserved.
//


import Foundation


///
/// Defines a network request conforming to the OAuth 2.0 standard access token request
/// for the authorization code grant flow.
/// https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.3
///
struct TokenRequest: NetworkRequest {

let path: String
let clientID: String
let authorizationCode: String
let grantType: String
let redirectURI: String
let codeVerifier: String

init(path: String = "/oauth/v2/token",
clientID: String,
authorizationCode: String,
grantType: String = "authorization_code",
redirectURI: String,
codeVerifier: String) {
self.path = path
self.clientID = clientID
self.authorizationCode = authorizationCode
self.grantType = grantType
self.redirectURI = redirectURI
self.codeVerifier = codeVerifier
}

// MARK: Request

var method: HTTPMethod {
.post
}

typealias Response = Token

var parameters: [String : String]? {
[
"code": authorizationCode,
"client_id": clientID,
"redirect_uri": redirectURI,
"grant_type": grantType,
"code_verifier": codeVerifier
]
}
}

///
/// The Access Token response for the authorization code grant flow as
/// defined by the OAuth 2.0 standard.
/// https://datatracker.ietf.org/doc/html/rfc6749#section-5.1
///
struct Token: Codable {

let accessToken: String
let tokenType: String
let expiresIn: Int?
let refreshToken: String?
let scope: [String]

enum CodingKeys: String, CodingKey {
case accessToken = "access_token"
case tokenType = "token_type"
case expiresIn = "expires_in"
case refreshToken = "refresh_token"
case scope
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.accessToken = try container.decode(String.self, forKey: .accessToken)
self.tokenType = try container.decode(String.self, forKey: .tokenType)
self.expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn)
self.refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken)

let scopeString = try container.decodeIfPresent(String.self, forKey: .scope)
self.scope = (scopeString ?? "")
.split(separator: " ")
.map(String.init)
}

init(accessToken: String,
tokenType: String,
expiresIn: Int? = nil,
refreshToken: String? = nil,
scope: [String] = []) {
self.accessToken = accessToken
self.tokenType = tokenType
self.expiresIn = expiresIn
self.refreshToken = refreshToken
self.scope = scope
}
}
Loading

0 comments on commit 97bbae3

Please sign in to comment.