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 Token exchange request #295

Merged
merged 2 commits into from
Apr 5, 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
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
Loading