Files
OAuthSwift/Sources/OAuthSwiftClient.swift

353 lines
19 KiB
Swift

//
// OAuthSwiftClient.swift
// OAuthSwift
//
// Created by Dongri Jin on 6/21/14.
// Copyright (c) 2014 Dongri Jin. All rights reserved.
//
import Foundation
public var OAuthSwiftDataEncoding: String.Encoding = .utf8
@objc public protocol OAuthSwiftRequestHandle {
func cancel()
}
open class OAuthSwiftClient: NSObject {
fileprivate(set) open var credential: OAuthSwiftCredential
open var paramsLocation: OAuthSwiftHTTPRequest.ParamsLocation = .authorizationHeader
/// Contains default URL session configuration
open var sessionFactory = URLSessionFactory()
static let separator: String = "\r\n"
static var separatorData: Data = {
return OAuthSwiftClient.separator.data(using: OAuthSwiftDataEncoding)!
}()
// MARK: init
public init(credential: OAuthSwiftCredential) {
self.credential = credential
}
public convenience init(consumerKey: String, consumerSecret: String, version: OAuthSwiftCredential.Version = .oauth1) {
let credential = OAuthSwiftCredential(consumerKey: consumerKey, consumerSecret: consumerSecret)
credential.version = version
self.init(credential: credential)
}
public convenience init(consumerKey: String, consumerSecret: String, oauthToken: String, oauthTokenSecret: String, version: OAuthSwiftCredential.Version) {
self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, version: version)
self.credential.oauthToken = oauthToken
self.credential.oauthTokenSecret = oauthTokenSecret
}
// MARK: client methods
@discardableResult
open func get(_ url: URLConvertible, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.request(url, method: .GET, parameters: parameters, headers: headers, completionHandler: completion)
}
@discardableResult
open func post(_ url: URLConvertible, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, body: Data? = nil, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.request(url, method: .POST, parameters: parameters, headers: headers, body: body, completionHandler: completion)
}
@discardableResult
open func put(_ url: URLConvertible, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, body: Data? = nil, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.request(url, method: .PUT, parameters: parameters, headers: headers, body: body, completionHandler: completion)
}
@discardableResult
open func delete(_ url: URLConvertible, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.request(url, method: .DELETE, parameters: parameters, headers: headers, completionHandler: completion)
}
@discardableResult
open func patch(_ url: URLConvertible, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.request(url, method: .PATCH, parameters: parameters, headers: headers, completionHandler: completion)
}
@discardableResult
open func request(_ url: URLConvertible, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, body: Data? = nil, checkTokenExpiration: Bool = true, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
if checkTokenExpiration && self.credential.isTokenExpired() {
completion?(.failure(.tokenExpired(error: nil)))
return nil
}
guard url.url != nil else {
completion?(.failure(.encodingError(urlString: url.string)))
return nil
}
if let request = makeRequest(url, method: method, parameters: parameters, headers: headers, body: body) {
request.start(completionHandler: completion)
return request
}
return nil
}
open func makeRequest(_ request: URLRequest) -> OAuthSwiftHTTPRequest {
let request = OAuthSwiftHTTPRequest(request: request, paramsLocation: self.paramsLocation, sessionFactory: self.sessionFactory)
request.config.updateRequest(credential: self.credential)
return request
}
open func makeRequest(_ url: URLConvertible, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, body: Data? = nil) -> OAuthSwiftHTTPRequest? {
guard let url = url.url else {
return nil // XXX failure not thrown here
}
let request = OAuthSwiftHTTPRequest(url: url, method: method, parameters: parameters, paramsLocation: self.paramsLocation, httpBody: body, headers: headers ?? [:], sessionFactory: self.sessionFactory)
request.config.updateRequest(credential: self.credential)
return request
}
@discardableResult
public func postImage(_ url: URLConvertible, parameters: OAuthSwift.Parameters, image: Data, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
return self.multiPartRequest(url: url, method: .POST, parameters: parameters, image: image, completionHandler: completion)
}
open func makeMultiPartRequest(_ url: URLConvertible, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters = [:], multiparts: [OAuthSwiftMultipartData] = [], headers: OAuthSwift.Headers? = nil) -> OAuthSwiftHTTPRequest? {
let boundary = "AS-boundary-\(arc4random())-\(arc4random())"
let type = "multipart/form-data; boundary=\(boundary)"
let body = self.multiDataFromObject(parameters, multiparts: multiparts, boundary: boundary)
var finalHeaders = [kHTTPHeaderContentType: type]
finalHeaders += headers ?? [:]
return makeRequest(url, method: method, parameters: parameters, headers: finalHeaders, body: body)
}
func multiPartRequest(url: URLConvertible, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters, image: Data, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
let multiparts = [ OAuthSwiftMultipartData(name: "media", data: image, fileName: "file", mimeType: "image/jpeg") ]
guard let request = makeMultiPartRequest(url, method: method, parameters: parameters, multiparts: multiparts) else {
return nil
}
request.start(completionHandler: completion)
return request
}
open func multiPartBody(from inputParameters: OAuthSwift.Parameters, boundary: String) -> Data {
var parameters = OAuthSwift.Parameters()
var multiparts = [OAuthSwiftMultipartData]()
for (key, value) in inputParameters {
if let data = value as? Data, key == "media" {
let sectionType = "image/jpeg"
let sectionFilename = "file"
multiparts.append(OAuthSwiftMultipartData(name: key, data: data, fileName: sectionFilename, mimeType: sectionType))
} else {
parameters[key] = value
}
}
return multiDataFromObject(parameters, multiparts: multiparts, boundary: boundary)
}
@discardableResult
open func postMultiPartRequest(_ url: URLConvertible, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters, headers: OAuthSwift.Headers? = nil, multiparts: [OAuthSwiftMultipartData] = [], checkTokenExpiration: Bool = true, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) -> OAuthSwiftRequestHandle? {
if checkTokenExpiration && self.credential.isTokenExpired() {
completion?(.failure(.tokenExpired(error: nil)))
return nil
}
if let request = makeMultiPartRequest(url, method: method, parameters: parameters, multiparts: multiparts, headers: headers) {
request.start(completionHandler: completion)
return request
}
return nil
}
func multiDataFromObject(_ object: OAuthSwift.Parameters, multiparts: [OAuthSwiftMultipartData], boundary: String) -> Data {
var data = Data()
let prefixString = "--\(boundary)\r\n"
let prefixData = prefixString.data(using: OAuthSwiftDataEncoding)!
for (key, value) in object {
guard let valueData = "\(value)".data(using: OAuthSwiftDataEncoding) else {
continue
}
data.append(prefixData)
let multipartData = OAuthSwiftMultipartData(name: key, data: valueData, fileName: nil, mimeType: nil)
data.append(multipartData, encoding: OAuthSwiftDataEncoding, separatorData: OAuthSwiftClient.separatorData)
}
for multipart in multiparts {
data.append(prefixData)
data.append(multipart, encoding: OAuthSwiftDataEncoding, separatorData: OAuthSwiftClient.separatorData)
}
let endingString = "--\(boundary)--\r\n"
let endingData = endingString.data(using: OAuthSwiftDataEncoding)!
data.append(endingData)
return data
}
// MARK: Refresh Token
@discardableResult
open func renewAccessToken(accessTokenUrl: URLConvertible?, withRefreshToken refreshToken: String, parameters: OAuthSwift.Parameters? = nil, headers: OAuthSwift.Headers? = nil, contentType: String? = nil, accessTokenBasicAuthentification: Bool = false, customKeypath: String? = nil, customAccessTokenParams: Dictionary<String,String>? = nil, completionHandler completion: @escaping OAuthSwift.TokenCompletionHandler) -> OAuthSwiftRequestHandle? {
// The current access token isn't needed anymore.
self.credential.oauthToken = ""
var parameters = parameters ?? OAuthSwift.Parameters()
parameters["client_id"] = self.credential.consumerKey
parameters["refresh_token"] = refreshToken
parameters["grant_type"] = "refresh_token"
// if exists, will add custom parameters if needed for the distant OAuth2 server
if let customParameters = customAccessTokenParams {
parameters += customParameters
}
// Omit the consumer secret if it's empty; this makes token renewal consistent with PKCE authorization.
if !self.credential.consumerSecret.isEmpty {
parameters["client_secret"] = self.credential.consumerSecret
}
OAuthSwift.log?.trace("Renew access token, parameters: \(parameters)")
return requestOAuthAccessToken(accessTokenUrl: accessTokenUrl, withParameters: parameters, headers: headers, contentType: contentType, accessTokenBasicAuthentification: accessTokenBasicAuthentification,customKeypath: customKeypath, completionHandler: completion)
}
func requestOAuthAccessToken(accessTokenUrl: URLConvertible?, withParameters parameters: OAuthSwift.Parameters, headers: OAuthSwift.Headers? = nil, contentType: String? = nil, accessTokenBasicAuthentification: Bool = false, customKeypath: String? = nil, customAccessTokenParams: Dictionary<String,String>? = nil, completionHandler completion: @escaping OAuthSwift.TokenCompletionHandler) -> OAuthSwiftRequestHandle? {
OAuthSwift.log?.trace("Request Oauth access token ...")
let completionHandler: OAuthSwiftHTTPRequest.CompletionHandler = { [weak self] result in
guard let this = self else {
OAuthSwift.retainError(completion)
return
}
switch result {
case .success(let response):
OAuthSwift.log?.trace("Oauth access token response ...")
let responseJSON: Any? = try? response.jsonObject(options: .mutableContainers)
var responseParameters: OAuthSwift.Parameters
if let jsonDico = responseJSON as? [String: Any] {
responseParameters = jsonDico
} else {
responseParameters = response.string?.parametersFromQueryString ?? [:]
}
// if response datas are not in the root of the json, ie nested in a keypath
// ex:
//{
// "status": [{integer} API response status],
// "body": {
// "access_token": [{string} Your new access_token],
// "expires_in": [{integer} Access token expiry delay in seconds],
// "token_type": [{string] HTTP Authorization Header format: Bearer],
// "scope": [{string} Scopes the user accepted],
// "refresh_token": [{string} Your new refresh_token],
// "userid": [{string} The ID of the user]
// }
//}
// here objects are nested into "body"
if let _customKeypath = customKeypath , let nestedResponseParameters = responseParameters[_customKeypath] as? [String: Any]{
responseParameters = nestedResponseParameters
}
guard let accessToken = responseParameters["access_token"] as? String else {
let message = NSLocalizedString("Could not get Access Token", comment: "Due to an error in the OAuth2 process, we couldn't get a valid token.")
OAuthSwift.log?.error("Could not get access token")
completion(.failure(.serverError(message: message)))
return
}
if let idToken = responseParameters["id_token"] as? String {
this.credential.idToken = idToken.safeStringByRemovingPercentEncoding
}
if let refreshToken = responseParameters["refresh_token"] as? String {
this.credential.oauthRefreshToken = refreshToken.safeStringByRemovingPercentEncoding
}
if let expiresIn = responseParameters["expires_in"] as? String, let offset = Double(expiresIn) {
this.credential.oauthTokenExpiresAt = Date(timeInterval: offset, since: Date())
} else if let expiresIn = responseParameters["expires_in"] as? Double {
this.credential.oauthTokenExpiresAt = Date(timeInterval: expiresIn, since: Date())
}
this.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding
completion(.success((this.credential, response, responseParameters)))
case .failure(let error):
completion(.failure(error))
}
}
guard let accessTokenUrl = accessTokenUrl else {
let message = NSLocalizedString("access token url not defined", comment: "access token url not defined with code type auth")
OAuthSwift.log?.error("Access token url not defined")
completion(.failure(.configurationError(message: message)))
return nil
}
if contentType == "multipart/form-data" {
// Request new access token by disabling check on current token expiration. This is safe because the implementation wants the user to retrieve a new token.
return self.postMultiPartRequest(accessTokenUrl, method: .POST, parameters: parameters, headers: headers, checkTokenExpiration: false, completionHandler: completionHandler)
} else {
// special headers
var finalHeaders: OAuthSwift.Headers? = headers
if accessTokenBasicAuthentification {
let authentification = "\(self.credential.consumerKey):\(self.credential.consumerSecret)".data(using: String.Encoding.utf8)
if let base64Encoded = authentification?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) {
finalHeaders += ["Authorization": "Basic \(base64Encoded)"] as OAuthSwift.Headers
}
}
// Request new access token by disabling check on current token expiration. This is safe because the implementation wants the user to retrieve a new token.
return self.request(accessTokenUrl, method: .POST, parameters: parameters, headers: finalHeaders, checkTokenExpiration: false, completionHandler: completionHandler)
}
}
open func requestWithAutomaticAccessTokenRenewal(url: URL, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters = [:], headers: OAuthSwift.Headers? = nil, contentType: String? = nil, accessTokenBasicAuthentification: Bool = false, accessTokenUrl: URLConvertible, onTokenRenewal: OAuthSwift.TokenRenewedHandler?, completionHandler completion: OAuthSwiftHTTPRequest.CompletionHandler?) {
self.request(url, method: method, parameters: parameters, headers: headers) { [weak self] result in
guard let this = self else {
OAuthSwift.retainError(completion)
return
}
switch result {
case .success(let response):
if let completion = completion {
completion(.success(response))
}
case .failure(let error):
switch error {
case OAuthSwiftError.tokenExpired:
if let onTokenRenewal = onTokenRenewal {
let renewCompletionHandler: OAuthSwift.TokenCompletionHandler = { result in
switch result {
case .success(let (credential, _, _)):
onTokenRenewal(.success(credential))
this.requestWithAutomaticAccessTokenRenewal(url: url, method: method, parameters: parameters, headers: headers, contentType: contentType, accessTokenBasicAuthentification: accessTokenBasicAuthentification, accessTokenUrl: accessTokenUrl, onTokenRenewal: nil, completionHandler: completion)
case .failure(let error):
if let completion = completion {
completion(.failure(.tokenExpired(error: error)))
}
}
}
_ = this.renewAccessToken(accessTokenUrl: accessTokenUrl, withRefreshToken: this.credential.oauthRefreshToken, headers: headers, contentType: contentType, accessTokenBasicAuthentification: accessTokenBasicAuthentification, completionHandler: renewCompletionHandler)
} else {
if let completion = completion {
completion(.failure(.tokenExpired(error: nil)))
}
}
default:
if let completion = completion {
completion(.failure(.tokenExpired(error: nil)))
}
}
}
}
}
}