Sources/UberAuth/Token/AccessToken.swift (112 lines of code) (raw):

// // AccessToken.swift // UberAuth // // Copyright © 2024 Uber Technologies, Inc. All rights reserved. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. // // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN // THE SOFTWARE. import Foundation /// /// 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 /// public struct AccessToken: Codable, Equatable { public let tokenString: String? public let refreshToken: String? public let tokenType: String? public let expiresIn: Int? public let scope: [String]? // MARK: Initializers public init(tokenString: String? = nil, refreshToken: String? = nil, tokenType: String? = nil, expiresIn: Int? = nil, scope: [String]? = nil) { self.tokenString = tokenString self.refreshToken = refreshToken self.tokenType = tokenType self.expiresIn = expiresIn self.scope = scope } public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) let tokenString = try container.decode(String.self, forKey: .tokenString) let tokenType = try container.decodeIfPresent(String.self, forKey: .tokenType) let expiresIn = try container.decodeIfPresent(Int.self, forKey: .expiresIn) let refreshToken = try container.decodeIfPresent(String.self, forKey: .refreshToken) let scope: [String]? if let scopeString = try? container.decodeIfPresent(String.self, forKey: .scope) { scope = (scopeString ?? "") .split(separator: " ") .map(String.init) } else { scope = try? container.decodeIfPresent([String].self, forKey: .scope) } self = AccessToken( tokenString: tokenString, refreshToken: refreshToken, tokenType: tokenType, expiresIn: expiresIn, scope: scope ) } enum CodingKeys: String, CodingKey { case tokenString = "access_token" case tokenType = "token_type" case expiresIn = "expires_in" case refreshToken = "refresh_token" case scope } } /// /// Initializers to build access tokens from various responses /// extension AccessToken { public init(jsonData: Data) throws { guard let responseDictionary = (try? JSONSerialization.jsonObject(with: jsonData, options: [])) as? [String: Any] else { throw UberAuthError.invalidResponse } self = try AccessToken(oAuthDictionary: responseDictionary) } /// Builds an AccessToken from the provided JSON data /// /// - Throws: UberAuthenticationError /// - Parameter jsonData: The JSON Data to parse the token from /// - Returns: An initialized AccessToken public init(oAuthDictionary: [String: Any]) throws { guard let tokenString = oAuthDictionary["access_token"] as? String else { throw UberAuthError.invalidResponse } self.tokenString = tokenString self.refreshToken = oAuthDictionary["refresh_token"] as? String self.tokenType = oAuthDictionary["token_type"] as? String self.expiresIn = { if let expiresIn = oAuthDictionary["expires_in"] as? Int { return expiresIn } if let expiresIn = oAuthDictionary["expires_in"] as? String, let expiresInInt = Int(expiresIn) { return expiresInInt } return 0 }() self.scope = ((oAuthDictionary["scope"] as? String) ?? "") .components(separatedBy: " ") .flatMap { $0.components(separatedBy: "+") } } /// Builds an AccessToken from the provided redirect URL /// /// - Throws: UberAuthenticationError /// - Parameter url: The URL to parse the token from /// - Returns: An initialized AccessToken, or nil if one couldn't be created public init(redirectURL: URL) throws { guard var components = URLComponents(url: redirectURL, resolvingAgainstBaseURL: false) else { throw UberAuthError.invalidResponse } var finalQueryArray = [String]() if let existingQuery = components.query { finalQueryArray.append(existingQuery) } if let existingFragment = components.fragment { finalQueryArray.append(existingFragment) } components.fragment = nil components.query = finalQueryArray.joined(separator: "&") guard let queryItems = components.queryItems else { throw UberAuthError.invalidRequest(redirectURL.absoluteString) } var queryDictionary = [String: Any]() for queryItem in queryItems { guard let value = queryItem.value else { continue } queryDictionary[queryItem.name] = value } self = try AccessToken(oAuthDictionary: queryDictionary) } } extension AccessToken: CustomStringConvertible { public var description: String { return """ Token String: \(tokenString ?? "nil") Refresh Token: \(refreshToken ?? "nil") Token Type: \(tokenType ?? "nil") Expires In: \(expiresIn ?? -1) Scopes: \(scope?.joined(separator: ", ") ?? "nil") """ } }