// // OAuth2Swift.swift // OAuthSwift // // Created by Dongri Jin on 6/22/14. // Copyright (c) 2014 Dongri Jin. All rights reserved. // import Foundation open class OAuth2Swift: OAuthSwift { /// If your oauth provider need to use basic authentification /// set value to true (default: false) open var accessTokenBasicAuthentification = false /// Set to true to deactivate state check. Be careful of CSRF open var allowMissingStateCheck: Bool = false /// Encode callback url, some services require it to be encoded. open var encodeCallbackURL: Bool = false /// Encode callback url inside the query, this is second encoding phase when the entire query string gets assembled. In rare /// cases, like with Imgur, the url needs to be encoded only once and this value needs to be set to `false`. open var encodeCallbackURLQuery: Bool = true var consumerKey: String var consumerSecret: String var authorizeUrl: String var accessTokenUrl: String? var responseType: String var contentType: String? // MARK: init public convenience init(consumerKey: String, consumerSecret: String, authorizeUrl: String, accessTokenUrl: String, responseType: String) { self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, responseType: responseType) self.accessTokenUrl = accessTokenUrl } public convenience init(consumerKey: String, consumerSecret: String, authorizeUrl: String, accessTokenUrl: String, responseType: String, contentType: String) { self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, responseType: responseType) self.accessTokenUrl = accessTokenUrl self.contentType = contentType } public init(consumerKey: String, consumerSecret: String, authorizeUrl: String, responseType: String) { self.consumerKey = consumerKey self.consumerSecret = consumerSecret self.authorizeUrl = authorizeUrl self.responseType = responseType super.init(consumerKey: consumerKey, consumerSecret: consumerSecret) self.client.credential.version = .oauth2 } public convenience init?(parameters: ConfigParameters) { guard let consumerKey = parameters["consumerKey"], let consumerSecret = parameters["consumerSecret"], let responseType = parameters["responseType"], let authorizeUrl = parameters["authorizeUrl"] else { return nil } if let accessTokenUrl = parameters["accessTokenUrl"] { self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, accessTokenUrl: accessTokenUrl, responseType: responseType) } else { self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, responseType: responseType) } } open var parameters: ConfigParameters { return [ "consumerKey": consumerKey, "consumerSecret": consumerSecret, "authorizeUrl": authorizeUrl, "accessTokenUrl": accessTokenUrl ?? "", "responseType": responseType ] } // MARK: functions @discardableResult open func authorize(withCallbackURL callbackURL: URL?, scope: String, state: String, parameters: Parameters = [:], headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? { self.observeCallback { [weak self] url in guard let this = self else { OAuthSwift.retainError(failure) return } var responseParameters = [String: String]() if let query = url.query { responseParameters += query.parametersFromQueryString } if let fragment = url.fragment, !fragment.isEmpty { responseParameters += fragment.parametersFromQueryString } if let accessToken = responseParameters["access_token"] { this.client.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding if let expiresIn: String = responseParameters["expires_in"], let offset = Double(expiresIn) { this.client.credential.oauthTokenExpiresAt = Date(timeInterval: offset, since: Date()) } success(this.client.credential, nil, responseParameters) } else if let code = responseParameters["code"] { if !this.allowMissingStateCheck { guard let responseState = responseParameters["state"] else { failure?(OAuthSwiftError.missingState) return } if responseState != state { failure?(OAuthSwiftError.stateNotEqual(state: state, responseState: responseState)) return } } let callbackURLEncoded: URL? if let callbackURL = callbackURL { callbackURLEncoded = URL(string: callbackURL.absoluteString.urlEncoded)! } else { callbackURLEncoded = nil } if let handle = this.postOAuthAccessTokenWithRequestToken( byCode: code.safeStringByRemovingPercentEncoding, callbackURL: callbackURLEncoded, headers: headers, success: success, failure: failure) { this.putHandle(handle, withKey: UUID().uuidString) } } else if let error = responseParameters["error"] { let description = responseParameters["error_description"] ?? "" let message = NSLocalizedString(error, comment: description) failure?(OAuthSwiftError.serverError(message: message)) } else { let message = "No access_token, no code and no error provided by server" failure?(OAuthSwiftError.serverError(message: message)) } } var queryErrorString = "" let encodeError: (String, String) -> Void = { name, value in if let newQuery = queryErrorString.urlQueryByAppending(parameter: name, value: value, encode: false) { queryErrorString = newQuery } } var queryString: String? = "" queryString = queryString?.urlQueryByAppending(parameter: "client_id", value: self.consumerKey, encodeError) if let callbackURL = callbackURL { queryString = queryString?.urlQueryByAppending(parameter: "redirect_uri", value: self.encodeCallbackURL ? callbackURL.absoluteString.urlEncoded : callbackURL.absoluteString, encode: self.encodeCallbackURLQuery, encodeError) } queryString = queryString?.urlQueryByAppending(parameter: "response_type", value: self.responseType, encodeError) queryString = queryString?.urlQueryByAppending(parameter: "scope", value: scope, encodeError) queryString = queryString?.urlQueryByAppending(parameter: "state", value: state, encodeError) for (name, value) in parameters { queryString = queryString?.urlQueryByAppending(parameter: name, value: "\(value)", encodeError) } if let queryString = queryString { let urlString = self.authorizeUrl.urlByAppending(query: queryString) if let url: URL = URL(string: urlString) { self.authorizeURLHandler.handle(url) return self } else { failure?(OAuthSwiftError.encodingError(urlString: urlString)) } } else { let urlString = self.authorizeUrl.urlByAppending(query: queryErrorString) failure?(OAuthSwiftError.encodingError(urlString: urlString)) } self.cancel() // ie. remove the observer. return nil } @discardableResult open func authorize(withCallbackURL urlString: String, scope: String, state: String, parameters: Parameters = [:], headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? { guard let url = URL(string: urlString) else { failure?(OAuthSwiftError.encodingError(urlString: urlString)) return nil } return authorize(withCallbackURL: url, scope: scope, state: state, parameters: parameters, headers: headers, success: success, failure: failure) } open func postOAuthAccessTokenWithRequestToken(byCode code: String, callbackURL: URL?, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? { var parameters = OAuthSwift.Parameters() parameters["client_id"] = self.consumerKey parameters["client_secret"] = self.consumerSecret parameters["code"] = code parameters["grant_type"] = "authorization_code" if let callbackURL = callbackURL { parameters["redirect_uri"] = callbackURL.absoluteString.safeStringByRemovingPercentEncoding } return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure) } @discardableResult open func renewAccessToken(withRefreshToken refreshToken: String, parameters: OAuthSwift.Parameters? = nil, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? { var parameters = parameters ?? OAuthSwift.Parameters() parameters["client_id"] = self.consumerKey parameters["client_secret"] = self.consumerSecret parameters["refresh_token"] = refreshToken parameters["grant_type"] = "refresh_token" return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure) } fileprivate func requestOAuthAccessToken(withParameters parameters: OAuthSwift.Parameters, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? { let successHandler: OAuthSwiftHTTPRequest.SuccessHandler = { [weak self] response in guard let this = self else { OAuthSwift.retainError(failure) return } let responseJSON: Any? = try? response.jsonObject(options: .mutableContainers) let responseParameters: OAuthSwift.Parameters if let jsonDico = responseJSON as? [String: Any] { responseParameters = jsonDico } else { responseParameters = response.string?.parametersFromQueryString ?? [:] } 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.") failure?(OAuthSwiftError.serverError(message: message)) return } if let refreshToken = responseParameters["refresh_token"] as? String { this.client.credential.oauthRefreshToken = refreshToken.safeStringByRemovingPercentEncoding } if let expiresIn = responseParameters["expires_in"] as? String, let offset = Double(expiresIn) { this.client.credential.oauthTokenExpiresAt = Date(timeInterval: offset, since: Date()) } else if let expiresIn = responseParameters["expires_in"] as? Double { this.client.credential.oauthTokenExpiresAt = Date(timeInterval: expiresIn, since: Date()) } this.client.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding success(this.client.credential, response, responseParameters) } guard let accessTokenUrl = accessTokenUrl else { let message = NSLocalizedString("access token url not defined", comment: "access token url not defined with code type auth") failure?(OAuthSwiftError.configurationError(message: message)) return nil } if self.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.client.postMultiPartRequest(accessTokenUrl, method: .POST, parameters: parameters, headers: headers, checkTokenExpiration: false, success: successHandler, failure: failure) } else { // special headers var finalHeaders: OAuthSwift.Headers? = headers if accessTokenBasicAuthentification { let authentification = "\(self.consumerKey):\(self.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.client.request(accessTokenUrl, method: .POST, parameters: parameters, headers: finalHeaders, checkTokenExpiration: false, success: successHandler, failure: failure) } } /** Convenience method to start a request that must be authorized with the previously retrieved access token. Since OAuth 2 requires support for the access token refresh mechanism, this method will take care to automatically refresh the token if needed such that the developer only has to be concerned about the outcome of the request. - parameter url: The url for the request. - parameter method: The HTTP method to use. - parameter parameters: The request's parameters. - parameter headers: The request's headers. - parameter renewHeaders: The request's headers if renewing. If nil, the `headers`` are used when renewing. - parameter body: The request's HTTP body. - parameter onTokenRenewal: Optional callback triggered in case the access token renewal was required in order to properly authorize the request. - parameter success: The success block. Takes the successfull response and data as parameter. - parameter failure: The failure block. Takes the error as parameter. */ @discardableResult open func startAuthorizedRequest(_ url: String, method: OAuthSwiftHTTPRequest.Method, parameters: OAuthSwift.Parameters, headers: OAuthSwift.Headers? = nil, renewHeaders: OAuthSwift.Headers? = nil, body: Data? = nil, onTokenRenewal: TokenRenewedHandler? = nil, success: @escaping OAuthSwiftHTTPRequest.SuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? { // build request return self.client.request(url, method: method, parameters: parameters, headers: headers, body: body, success: success) { (error) in switch error { case OAuthSwiftError.tokenExpired: _ = self.renewAccessToken(withRefreshToken: self.client.credential.oauthRefreshToken, headers: renewHeaders ?? headers, success: { (credential, _, _) in // Ommit response parameters so they don't override the original ones // We have successfully renewed the access token. // If provided, fire the onRenewal closure if let renewalCallBack = onTokenRenewal { renewalCallBack(credential) } // Reauthorize the request again, this time with a brand new access token ready to be used. _ = self.startAuthorizedRequest(url, method: method, parameters: parameters, headers: headers, body: body, onTokenRenewal: onTokenRenewal, success: success, failure: failure) }, failure: failure) default: failure(error) } } } // OAuth 2.0 Specification: https://tools.ietf.org/html/draft-ietf-oauth-v2-13#section-4.3 @discardableResult open func authorize(username: String, password: String, scope: String?, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? { var parameters = OAuthSwift.Parameters() parameters["client_id"] = self.consumerKey parameters["client_secret"] = self.consumerSecret parameters["username"] = username parameters["password"] = password parameters["grant_type"] = "password" if let scope = scope { parameters["scope"] = scope } return requestOAuthAccessToken( withParameters: parameters, headers: headers, success: success, failure: failure ) } @discardableResult open func authorize(deviceToken deviceCode: String, grantType: String = "http://oauth.net/grant_type/device/1.0", success: @escaping TokenSuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? { var parameters = OAuthSwift.Parameters() parameters["client_id"] = self.consumerKey parameters["client_secret"] = self.consumerSecret parameters["code"] = deviceCode parameters["grant_type"] = grantType return requestOAuthAccessToken( withParameters: parameters, success: success, failure: failure ) } }