5 // Created by Dongri Jin on 6/22/14.
6 // Copyright (c) 2014 Dongri Jin. All rights reserved.
11 open class OAuth2Swift: OAuthSwift {
13 /// If your oauth provider need to use basic authentification
14 /// set value to true (default: false)
15 open var accessTokenBasicAuthentification = false
17 /// Set to true to deactivate state check. Be careful of CSRF
18 open var allowMissingStateCheck: Bool = false
20 /// Encode callback url, some services require it to be encoded.
21 open var encodeCallbackURL: Bool = false
23 /// Encode callback url inside the query, this is second encoding phase when the entire query string gets assembled. In rare
24 /// cases, like with Imgur, the url needs to be encoded only once and this value needs to be set to `false`.
25 open var encodeCallbackURLQuery: Bool = true
27 var consumerKey: String
28 var consumerSecret: String
29 var authorizeUrl: String
30 var accessTokenUrl: String?
31 var responseType: String
32 var contentType: String?
35 public convenience init(consumerKey: String, consumerSecret: String, authorizeUrl: String, accessTokenUrl: String, responseType: String) {
36 self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, responseType: responseType)
37 self.accessTokenUrl = accessTokenUrl
40 public convenience init(consumerKey: String, consumerSecret: String, authorizeUrl: String, accessTokenUrl: String, responseType: String, contentType: String) {
41 self.init(consumerKey: consumerKey, consumerSecret: consumerSecret, authorizeUrl: authorizeUrl, responseType: responseType)
42 self.accessTokenUrl = accessTokenUrl
43 self.contentType = contentType
46 public init(consumerKey: String, consumerSecret: String, authorizeUrl: String, responseType: String) {
47 self.consumerKey = consumerKey
48 self.consumerSecret = consumerSecret
49 self.authorizeUrl = authorizeUrl
50 self.responseType = responseType
51 super.init(consumerKey: consumerKey, consumerSecret: consumerSecret)
52 self.client.credential.version = .oauth2
55 public convenience init?(parameters: ConfigParameters) {
56 guard let consumerKey = parameters["consumerKey"], let consumerSecret = parameters["consumerSecret"],
57 let responseType = parameters["responseType"], let authorizeUrl = parameters["authorizeUrl"] else {
60 if let accessTokenUrl = parameters["accessTokenUrl"] {
61 self.init(consumerKey: consumerKey, consumerSecret: consumerSecret,
62 authorizeUrl: authorizeUrl, accessTokenUrl: accessTokenUrl, responseType: responseType)
64 self.init(consumerKey: consumerKey, consumerSecret: consumerSecret,
65 authorizeUrl: authorizeUrl, responseType: responseType)
69 open var parameters: ConfigParameters {
71 "consumerKey": consumerKey,
72 "consumerSecret": consumerSecret,
73 "authorizeUrl": authorizeUrl,
74 "accessTokenUrl": accessTokenUrl ?? "",
75 "responseType": responseType
81 open func authorize(withCallbackURL callbackURL: URL?, scope: String, state: String, parameters: Parameters = [:], headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
83 self.observeCallback { [weak self] url in
84 guard let this = self else {
85 OAuthSwift.retainError(failure)
88 var responseParameters = [String: String]()
89 if let query = url.query {
90 responseParameters += query.parametersFromQueryString
92 if let fragment = url.fragment, !fragment.isEmpty {
93 responseParameters += fragment.parametersFromQueryString
95 if let accessToken = responseParameters["access_token"] {
96 this.client.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding
97 if let expiresIn: String = responseParameters["expires_in"], let offset = Double(expiresIn) {
98 this.client.credential.oauthTokenExpiresAt = Date(timeInterval: offset, since: Date())
100 success(this.client.credential, nil, responseParameters)
101 } else if let code = responseParameters["code"] {
102 if !this.allowMissingStateCheck {
103 guard let responseState = responseParameters["state"] else {
104 failure?(OAuthSwiftError.missingState)
107 if responseState != state {
108 failure?(OAuthSwiftError.stateNotEqual(state: state, responseState: responseState))
112 let callbackURLEncoded: URL?
113 if let callbackURL = callbackURL {
114 callbackURLEncoded = URL(string: callbackURL.absoluteString.urlEncoded)!
116 callbackURLEncoded = nil
118 if let handle = this.postOAuthAccessTokenWithRequestToken(
119 byCode: code.safeStringByRemovingPercentEncoding,
120 callbackURL: callbackURLEncoded, headers: headers, success: success, failure: failure) {
121 this.putHandle(handle, withKey: UUID().uuidString)
123 } else if let error = responseParameters["error"] {
124 let description = responseParameters["error_description"] ?? ""
125 let message = NSLocalizedString(error, comment: description)
126 failure?(OAuthSwiftError.serverError(message: message))
128 let message = "No access_token, no code and no error provided by server"
129 failure?(OAuthSwiftError.serverError(message: message))
133 var queryErrorString = ""
134 let encodeError: (String, String) -> Void = { name, value in
135 if let newQuery = queryErrorString.urlQueryByAppending(parameter: name, value: value, encode: false) {
136 queryErrorString = newQuery
140 var queryString: String? = ""
141 queryString = queryString?.urlQueryByAppending(parameter: "client_id", value: self.consumerKey, encodeError)
142 if let callbackURL = callbackURL {
143 queryString = queryString?.urlQueryByAppending(parameter: "redirect_uri", value: self.encodeCallbackURL ? callbackURL.absoluteString.urlEncoded : callbackURL.absoluteString, encode: self.encodeCallbackURLQuery, encodeError)
145 queryString = queryString?.urlQueryByAppending(parameter: "response_type", value: self.responseType, encodeError)
146 queryString = queryString?.urlQueryByAppending(parameter: "scope", value: scope, encodeError)
147 queryString = queryString?.urlQueryByAppending(parameter: "state", value: state, encodeError)
149 for (name, value) in parameters {
150 queryString = queryString?.urlQueryByAppending(parameter: name, value: "\(value)", encodeError)
153 if let queryString = queryString {
154 let urlString = self.authorizeUrl.urlByAppending(query: queryString)
155 if let url: URL = URL(string: urlString) {
156 self.authorizeURLHandler.handle(url)
159 failure?(OAuthSwiftError.encodingError(urlString: urlString))
162 let urlString = self.authorizeUrl.urlByAppending(query: queryErrorString)
163 failure?(OAuthSwiftError.encodingError(urlString: urlString))
165 self.cancel() // ie. remove the observer.
170 open func authorize(withCallbackURL urlString: String, scope: String, state: String, parameters: Parameters = [:], headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
171 guard let url = URL(string: urlString) else {
172 failure?(OAuthSwiftError.encodingError(urlString: urlString))
175 return authorize(withCallbackURL: url, scope: scope, state: state, parameters: parameters, headers: headers, success: success, failure: failure)
178 open func postOAuthAccessTokenWithRequestToken(byCode code: String, callbackURL: URL?, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
179 var parameters = OAuthSwift.Parameters()
180 parameters["client_id"] = self.consumerKey
181 parameters["client_secret"] = self.consumerSecret
182 parameters["code"] = code
183 parameters["grant_type"] = "authorization_code"
184 if let callbackURL = callbackURL {
185 parameters["redirect_uri"] = callbackURL.absoluteString.safeStringByRemovingPercentEncoding
188 return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure)
192 open func renewAccessToken(withRefreshToken refreshToken: String, parameters: OAuthSwift.Parameters? = nil, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
193 var parameters = parameters ?? OAuthSwift.Parameters()
194 parameters["client_id"] = self.consumerKey
195 parameters["client_secret"] = self.consumerSecret
196 parameters["refresh_token"] = refreshToken
197 parameters["grant_type"] = "refresh_token"
199 return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure)
202 fileprivate func requestOAuthAccessToken(withParameters parameters: OAuthSwift.Parameters, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
203 let successHandler: OAuthSwiftHTTPRequest.SuccessHandler = { [weak self] response in
204 guard let this = self else {
205 OAuthSwift.retainError(failure)
208 let responseJSON: Any? = try? response.jsonObject(options: .mutableContainers)
210 let responseParameters: OAuthSwift.Parameters
212 if let jsonDico = responseJSON as? [String: Any] {
213 responseParameters = jsonDico
215 responseParameters = response.string?.parametersFromQueryString ?? [:]
218 guard let accessToken = responseParameters["access_token"] as? String else {
219 let message = NSLocalizedString("Could not get Access Token", comment: "Due to an error in the OAuth2 process, we couldn't get a valid token.")
220 failure?(OAuthSwiftError.serverError(message: message))
224 if let refreshToken = responseParameters["refresh_token"] as? String {
225 this.client.credential.oauthRefreshToken = refreshToken.safeStringByRemovingPercentEncoding
228 if let expiresIn = responseParameters["expires_in"] as? String, let offset = Double(expiresIn) {
229 this.client.credential.oauthTokenExpiresAt = Date(timeInterval: offset, since: Date())
230 } else if let expiresIn = responseParameters["expires_in"] as? Double {
231 this.client.credential.oauthTokenExpiresAt = Date(timeInterval: expiresIn, since: Date())
234 this.client.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding
235 success(this.client.credential, response, responseParameters)
238 guard let accessTokenUrl = accessTokenUrl else {
239 let message = NSLocalizedString("access token url not defined", comment: "access token url not defined with code type auth")
240 failure?(OAuthSwiftError.configurationError(message: message))
244 if self.contentType == "multipart/form-data" {
245 // 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.
246 return self.client.postMultiPartRequest(accessTokenUrl, method: .POST, parameters: parameters, headers: headers, checkTokenExpiration: false, success: successHandler, failure: failure)
249 var finalHeaders: OAuthSwift.Headers? = headers
250 if accessTokenBasicAuthentification {
252 let authentification = "\(self.consumerKey):\(self.consumerSecret)".data(using: String.Encoding.utf8)
253 if let base64Encoded = authentification?.base64EncodedString(options: Data.Base64EncodingOptions(rawValue: 0)) {
254 finalHeaders += ["Authorization": "Basic \(base64Encoded)"] as OAuthSwift.Headers
258 // 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.
259 return self.client.request(accessTokenUrl, method: .POST, parameters: parameters, headers: finalHeaders, checkTokenExpiration: false, success: successHandler, failure: failure)
264 Convenience method to start a request that must be authorized with the previously retrieved access token.
265 Since OAuth 2 requires support for the access token refresh mechanism, this method will take care to automatically
266 refresh the token if needed such that the developer only has to be concerned about the outcome of the request.
268 - parameter url: The url for the request.
269 - parameter method: The HTTP method to use.
270 - parameter parameters: The request's parameters.
271 - parameter headers: The request's headers.
272 - parameter renewHeaders: The request's headers if renewing. If nil, the `headers`` are used when renewing.
273 - parameter body: The request's HTTP body.
274 - parameter onTokenRenewal: Optional callback triggered in case the access token renewal was required in order to properly authorize the request.
275 - parameter success: The success block. Takes the successfull response and data as parameter.
276 - parameter failure: The failure block. Takes the error as parameter.
279 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? {
281 return self.client.request(url, method: method, parameters: parameters, headers: headers, body: body, success: success) { (error) in
284 case OAuthSwiftError.tokenExpired:
285 _ = self.renewAccessToken(withRefreshToken: self.client.credential.oauthRefreshToken, headers: renewHeaders ?? headers, success: { (credential, _, _) in
286 // Ommit response parameters so they don't override the original ones
287 // We have successfully renewed the access token.
289 // If provided, fire the onRenewal closure
290 if let renewalCallBack = onTokenRenewal {
291 renewalCallBack(credential)
294 // Reauthorize the request again, this time with a brand new access token ready to be used.
295 _ = self.startAuthorizedRequest(url, method: method, parameters: parameters, headers: headers, body: body, onTokenRenewal: onTokenRenewal, success: success, failure: failure)
303 // OAuth 2.0 Specification: https://tools.ietf.org/html/draft-ietf-oauth-v2-13#section-4.3
305 open func authorize(username: String, password: String, scope: String?, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? {
307 var parameters = OAuthSwift.Parameters()
308 parameters["client_id"] = self.consumerKey
309 parameters["client_secret"] = self.consumerSecret
310 parameters["username"] = username
311 parameters["password"] = password
312 parameters["grant_type"] = "password"
314 if let scope = scope {
315 parameters["scope"] = scope
318 return requestOAuthAccessToken(
319 withParameters: parameters,
327 open func authorize(deviceToken deviceCode: String, grantType: String = "http://oauth.net/grant_type/device/1.0", success: @escaping TokenSuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? {
328 var parameters = OAuthSwift.Parameters()
329 parameters["client_id"] = self.consumerKey
330 parameters["client_secret"] = self.consumerSecret
331 parameters["code"] = deviceCode
332 parameters["grant_type"] = grantType
334 return requestOAuthAccessToken(
335 withParameters: parameters,