added iOS source code
[wl-app.git] / iOS / Pods / OAuthSwift / Sources / OAuth2Swift.swift
1 //
2 //  OAuth2Swift.swift
3 //  OAuthSwift
4 //
5 //  Created by Dongri Jin on 6/22/14.
6 //  Copyright (c) 2014 Dongri Jin. All rights reserved.
7 //
8
9 import Foundation
10
11 open class OAuth2Swift: OAuthSwift {
12
13     /// If your oauth provider need to use basic authentification
14     /// set value to true (default: false)
15     open var accessTokenBasicAuthentification = false
16
17     /// Set to true to deactivate state check. Be careful of CSRF
18     open var allowMissingStateCheck: Bool = false
19
20     /// Encode callback url, some services require it to be encoded.
21     open var encodeCallbackURL: Bool = false
22
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
26
27     var consumerKey: String
28     var consumerSecret: String
29     var authorizeUrl: String
30     var accessTokenUrl: String?
31     var responseType: String
32     var contentType: String?
33
34     // MARK: init
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
38     }
39
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
44     }
45
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
53     }
54
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 {
58             return nil
59         }
60         if let accessTokenUrl = parameters["accessTokenUrl"] {
61             self.init(consumerKey: consumerKey, consumerSecret: consumerSecret,
62                 authorizeUrl: authorizeUrl, accessTokenUrl: accessTokenUrl, responseType: responseType)
63         } else {
64             self.init(consumerKey: consumerKey, consumerSecret: consumerSecret,
65                 authorizeUrl: authorizeUrl, responseType: responseType)
66         }
67     }
68
69     open var parameters: ConfigParameters {
70         return [
71             "consumerKey": consumerKey,
72             "consumerSecret": consumerSecret,
73             "authorizeUrl": authorizeUrl,
74             "accessTokenUrl": accessTokenUrl ?? "",
75             "responseType": responseType
76         ]
77     }
78
79     // MARK: functions
80     @discardableResult
81     open func authorize(withCallbackURL callbackURL: URL?, scope: String, state: String, parameters: Parameters = [:], headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: FailureHandler?) -> OAuthSwiftRequestHandle? {
82
83         self.observeCallback { [weak self] url in
84             guard let this = self else {
85                 OAuthSwift.retainError(failure)
86                 return
87             }
88             var responseParameters = [String: String]()
89             if let query = url.query {
90                 responseParameters += query.parametersFromQueryString
91             }
92             if let fragment = url.fragment, !fragment.isEmpty {
93                 responseParameters += fragment.parametersFromQueryString
94             }
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())
99                 }
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)
105                         return
106                     }
107                     if responseState != state {
108                         failure?(OAuthSwiftError.stateNotEqual(state: state, responseState: responseState))
109                         return
110                     }
111                 }
112                 let callbackURLEncoded: URL?
113                 if let callbackURL = callbackURL {
114                     callbackURLEncoded = URL(string: callbackURL.absoluteString.urlEncoded)!
115                 } else {
116                     callbackURLEncoded = nil
117                 }
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)
122                 }
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))
127             } else {
128                 let message = "No access_token, no code and no error provided by server"
129                 failure?(OAuthSwiftError.serverError(message: message))
130             }
131         }
132
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
137             }
138         }
139
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)
144         }
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)
148
149         for (name, value) in parameters {
150             queryString = queryString?.urlQueryByAppending(parameter: name, value: "\(value)", encodeError)
151         }
152
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)
157                 return self
158             } else {
159                 failure?(OAuthSwiftError.encodingError(urlString: urlString))
160             }
161         } else {
162             let urlString = self.authorizeUrl.urlByAppending(query: queryErrorString)
163             failure?(OAuthSwiftError.encodingError(urlString: urlString))
164         }
165         self.cancel() // ie. remove the observer.
166         return nil
167     }
168
169     @discardableResult
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))
173             return nil
174         }
175         return authorize(withCallbackURL: url, scope: scope, state: state, parameters: parameters, headers: headers, success: success, failure: failure)
176     }
177
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
186         }
187
188         return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure)
189     }
190
191     @discardableResult
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"
198
199         return requestOAuthAccessToken(withParameters: parameters, headers: headers, success: success, failure: failure)
200     }
201
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)
206                 return
207             }
208             let responseJSON: Any? = try? response.jsonObject(options: .mutableContainers)
209
210             let responseParameters: OAuthSwift.Parameters
211
212             if let jsonDico = responseJSON as? [String: Any] {
213                 responseParameters = jsonDico
214             } else {
215                 responseParameters = response.string?.parametersFromQueryString ?? [:]
216             }
217
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))
221                 return
222             }
223
224             if let refreshToken = responseParameters["refresh_token"] as? String {
225                 this.client.credential.oauthRefreshToken = refreshToken.safeStringByRemovingPercentEncoding
226             }
227
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())
232             }
233
234             this.client.credential.oauthToken = accessToken.safeStringByRemovingPercentEncoding
235             success(this.client.credential, response, responseParameters)
236         }
237
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))
241             return nil
242         }
243
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)
247         } else {
248             // special headers
249             var finalHeaders: OAuthSwift.Headers? = headers
250             if accessTokenBasicAuthentification {
251
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
255                 }
256             }
257
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)
260         }
261     }
262
263     /**
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.
267      
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.
277      */
278     @discardableResult
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? {
280         // build request
281         return self.client.request(url, method: method, parameters: parameters, headers: headers, body: body, success: success) { (error) in
282             switch error {
283
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.
288
289                     // If provided, fire the onRenewal closure
290                     if let renewalCallBack = onTokenRenewal {
291                         renewalCallBack(credential)
292                     }
293
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)
296                 }, failure: failure)
297             default:
298                 failure(error)
299             }
300         }
301     }
302
303         // OAuth 2.0 Specification: https://tools.ietf.org/html/draft-ietf-oauth-v2-13#section-4.3
304     @discardableResult
305     open func authorize(username: String, password: String, scope: String?, headers: OAuthSwift.Headers? = nil, success: @escaping TokenSuccessHandler, failure: @escaping OAuthSwiftHTTPRequest.FailureHandler) -> OAuthSwiftRequestHandle? {
306
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"
313
314         if let scope = scope {
315             parameters["scope"] = scope
316         }
317
318         return requestOAuthAccessToken(
319             withParameters: parameters,
320             headers: headers,
321             success: success,
322             failure: failure
323         )
324     }
325
326     @discardableResult
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
333
334         return requestOAuthAccessToken(
335             withParameters: parameters,
336             success: success,
337             failure: failure
338         )
339     }
340
341 }