forked from aerogear/aerogear-ios-oauth2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
OAuth2Module.swift
359 lines (303 loc) · 15.1 KB
/
OAuth2Module.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
/*
* JBoss, Home of Professional Open Source.
* Copyright Red Hat, Inc., and individual contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import Foundation
import UIKit
import AeroGearHttp
/**
Notification constants emitted during oauth authorization flow
*/
public let AGAppLaunchedWithURLNotification = "AGAppLaunchedWithURLNotification"
public let AGAppDidBecomeActiveNotification = "AGAppDidBecomeActiveNotification"
public let AGAuthzErrorDomain = "AGAuthzErrorDomain"
/**
The current state that this module is in
- AuthorizationStatePendingExternalApproval: the module is waiting external approval
- AuthorizationStateApproved: the oauth flow has been approved
- AuthorizationStateUnknown: the oauth flow is in unknown state (e.g. user clicked cancel)
*/
enum AuthorizationState {
case AuthorizationStatePendingExternalApproval
case AuthorizationStateApproved
case AuthorizationStateUnknown
}
/**
Parent class of any OAuth2 module implementing generic OAuth2 authorization flow
*/
public class OAuth2Module: AuthzModule {
let config: Config
var http: Http
var oauth2Session: OAuth2Session
var applicationLaunchNotificationObserver: NSObjectProtocol?
var applicationDidBecomeActiveNotificationObserver: NSObjectProtocol?
var state: AuthorizationState
var webView: WebViewController?
/**
Initialize an OAuth2 module
:param: config the configuration object that setups the module
:param: session the session that that module will be bound to
:param: requestSerializer the actual request serializer to use when performing requests
:param: responseSerializer the actual response serializer to use upon receiving a response
:returns: the newly initialized OAuth2Module
*/
public required init(config: Config, session: OAuth2Session? = nil, requestSerializer: RequestSerializer = HttpRequestSerializer(), responseSerializer: ResponseSerializer = JsonResponseSerializer()) {
if (config.accountId == nil) {
config.accountId = "ACCOUNT_FOR_CLIENTID_\(config.clientId)"
}
if (session == nil) {
self.oauth2Session = TrustedPersistantOAuth2Session(accountId: config.accountId!)
} else {
self.oauth2Session = session!
}
self.config = config
if config.isWebView {
self.webView = WebViewController()
}
self.http = Http(baseURL: config.baseURL, requestSerializer: requestSerializer, responseSerializer: responseSerializer)
self.state = .AuthorizationStateUnknown
}
// MARK: Public API - To be overriden if necessary by OAuth2 specific adapter
/**
Request an authorization code
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func requestAuthorizationCode(completionHandler: (AnyObject?, NSError?) -> Void) {
// register with the notification system in order to be notified when the 'authorization' process completes in the
// external browser, and the oauth code is available so that we can then proceed to request the 'access_token'
// from the server.
applicationLaunchNotificationObserver = NSNotificationCenter.defaultCenter().addObserverForName(AGAppLaunchedWithURLNotification, object: nil, queue: nil, usingBlock: { (notification: NSNotification!) -> Void in
self.extractCode(notification, completionHandler: completionHandler)
if ( self.webView != nil ) {
UIApplication.sharedApplication().keyWindow?.rootViewController?.dismissViewControllerAnimated(true, nil)
}
})
// register to receive notification when the application becomes active so we
// can clear any pending authorization requests which are not completed properly,
// that is a user switched into the app without Accepting or Cancelling the authorization
// request in the external browser process.
applicationDidBecomeActiveNotificationObserver = NSNotificationCenter.defaultCenter().addObserverForName(AGAppDidBecomeActiveNotification, object:nil, queue:nil, usingBlock: { (note: NSNotification!) -> Void in
// check the state
if (self.state == .AuthorizationStatePendingExternalApproval) {
// unregister
self.stopObserving()
// ..and update state
self.state = .AuthorizationStateUnknown;
}
})
// update state to 'Pending'
self.state = .AuthorizationStatePendingExternalApproval
// calculate final url
let params = "?scope=\(config.scope)&redirect_uri=\(config.redirectURL.urlEncode())&client_id=\(config.clientId)&response_type=code"
let url = NSURL(string:http.calculateURL(config.baseURL, url:config.authzEndpoint).absoluteString! + params)
if let url = url {
if self.webView != nil {
self.webView!.targetURL = url
UIApplication.sharedApplication().keyWindow?.rootViewController?.presentViewController(self.webView!, animated: true, completion: nil)
} else {
UIApplication.sharedApplication().openURL(url)
}
}
}
/**
Request to refresh an access token
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func refreshAccessToken(completionHandler: (AnyObject?, NSError?) -> Void) {
if let unwrappedRefreshToken = self.oauth2Session.refreshToken {
var paramDict: [String: String] = ["refresh_token": unwrappedRefreshToken, "client_id": config.clientId, "grant_type": "refresh_token"]
if (config.clientSecret != nil) {
paramDict["client_secret"] = config.clientSecret!
}
http.POST(config.refreshTokenEndpoint!, parameters: paramDict, completionHandler: { (response, error) in
if (error != nil) {
completionHandler(nil, error)
return
}
if let unwrappedResponse = response as? [String: AnyObject] {
let accessToken: String = unwrappedResponse["access_token"] as NSString
let expiration = unwrappedResponse["expires_in"] as NSNumber
let exp: String = expiration.stringValue
self.oauth2Session.saveAccessToken(accessToken, refreshToken: unwrappedRefreshToken, accessTokenExpiration: exp, refreshTokenExpiration: nil)
completionHandler(unwrappedResponse["access_token"], nil);
}
})
}
}
/**
Exchange an authorization code for an access token
:param: code the 'authorization' code to exchange for an access token
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func exchangeAuthorizationCodeForAccessToken(code: String, completionHandler: (AnyObject?, NSError?) -> Void) {
var paramDict: [String: String] = ["code": code, "client_id": config.clientId, "redirect_uri": config.redirectURL, "grant_type":"authorization_code"]
if let unwrapped = config.clientSecret {
paramDict["client_secret"] = unwrapped
}
http.POST(config.accessTokenEndpoint, parameters: paramDict, completionHandler: {(responseObject, error) in
if (error != nil) {
completionHandler(nil, error)
return
}
if let unwrappedResponse = responseObject as? [String: AnyObject] {
let accessToken: String = unwrappedResponse["access_token"] as NSString
let refreshToken: String = unwrappedResponse["refresh_token"] as NSString
let expiration = unwrappedResponse["expires_in"] as NSNumber
let exp: String = expiration.stringValue
// expiration for refresh token is used in Keycloak
let expirationRefresh = unwrappedResponse["refresh_expires_in"] as? NSNumber
let expRefresh = expirationRefresh?.stringValue
self.oauth2Session.saveAccessToken(accessToken, refreshToken: refreshToken, accessTokenExpiration: exp, refreshTokenExpiration: expRefresh)
completionHandler(accessToken, nil)
}
})
}
/**
Gateway to request authorization access
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func requestAccess(completionHandler: (AnyObject?, NSError?) -> Void) {
if (self.oauth2Session.accessToken != nil && self.oauth2Session.tokenIsNotExpired()) {
// we already have a valid access token, nothing more to be done
completionHandler(self.oauth2Session.accessToken!, nil);
} else if (self.oauth2Session.refreshToken != nil && self.oauth2Session.refreshTokenIsNotExpired()) {
// need to refresh token
self.refreshAccessToken(completionHandler)
} else {
// ask for authorization code and once obtained exchange code for access token
self.requestAuthorizationCode(completionHandler)
}
}
/**
Gateway to provide authentication using the Authorization Code Flow with OpenID Connect
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func login(completionHandler: (AnyObject?, OpenIDClaim?, NSError?) -> Void) {
self.requestAccess { (response:AnyObject?, error:NSError?) -> Void in
if (error != nil) {
completionHandler(nil, nil, error)
return
}
var paramDict: [String: String] = [:]
if response != nil {
paramDict = ["access_token": response! as String]
}
if let userInfoEndpoint = self.config.userInfoEndpoint {
self.http.GET(userInfoEndpoint, parameters: paramDict, completionHandler: {(responseObject, error) in
if (error != nil) {
completionHandler(nil, nil, error)
return
}
var openIDClaims: OpenIDClaim?
if let unwrappedResponse = responseObject as? [String: AnyObject] {
openIDClaims = OpenIDClaim(fromDict: unwrappedResponse)
}
completionHandler(response, openIDClaims, nil)
})
} else {
completionHandler(nil, nil, NSError(domain: "OAuth2Module", code: 0, userInfo: ["OpenID Connect" : "No UserInfo endpoint available in config"]))
return
}
}
}
/**
Request to revoke access
:param: completionHandler A block object to be executed when the request operation finishes.
*/
public func revokeAccess(completionHandler: (AnyObject?, NSError?) -> Void) {
// return if not yet initialized
if (self.oauth2Session.accessToken == nil) {
return;
}
let paramDict:[String:String] = ["token":self.oauth2Session.accessToken!]
http.POST(config.revokeTokenEndpoint!, parameters: paramDict, completionHandler: { (response, error) in
if (error != nil) {
completionHandler(nil, error)
return
}
self.oauth2Session.clearTokens()
completionHandler(response, nil)
})
}
/**
Return any authorization fields
:returns: a dictionary filled with the authorization fields
*/
public func authorizationFields() -> [String: String]? {
if (self.oauth2Session.accessToken == nil) {
return nil
} else {
return ["Authorization":"Bearer \(self.oauth2Session.accessToken!)"]
}
}
/**
Returns a boolean indicating whether authorization has been granted
:returns: true if authorized, false otherwise
*/
public func isAuthorized() -> Bool {
return self.oauth2Session.accessToken != nil && self.oauth2Session.tokenIsNotExpired()
}
// MARK: Internal Methods
func extractCode(notification: NSNotification, completionHandler: (AnyObject?, NSError?) -> Void) {
let url: NSURL? = (notification.userInfo as [String: AnyObject])[UIApplicationLaunchOptionsURLKey] as? NSURL
// extract the code from the URL
let code = self.parametersFromQueryString(url?.query)["code"]
// if exists perform the exchange
if (code != nil) {
self.exchangeAuthorizationCodeForAccessToken(code!, completionHandler: completionHandler)
// update state
state = .AuthorizationStateApproved
} else {
let error = NSError(domain:AGAuthzErrorDomain, code:0, userInfo:["NSLocalizedDescriptionKey": "User cancelled authorization."])
completionHandler(nil, error)
}
// finally, unregister
self.stopObserving()
}
func parametersFromQueryString(queryString: String?) -> [String: String] {
var parameters = [String: String]()
if (queryString != nil) {
var parameterScanner: NSScanner = NSScanner(string: queryString!)
var name:NSString? = nil
var value:NSString? = nil
while (parameterScanner.atEnd != true) {
name = nil;
parameterScanner.scanUpToString("=", intoString: &name)
parameterScanner.scanString("=", intoString:nil)
value = nil
parameterScanner.scanUpToString("&", intoString:&value)
parameterScanner.scanString("&", intoString:nil)
if (name != nil && value != nil) {
parameters[name!.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)!] = value!.stringByReplacingPercentEscapesUsingEncoding(NSUTF8StringEncoding)
}
}
}
return parameters;
}
deinit {
self.stopObserving()
}
func stopObserving() {
// clear all observers
if (applicationLaunchNotificationObserver != nil) {
NSNotificationCenter.defaultCenter().removeObserver(applicationLaunchNotificationObserver!)
self.applicationLaunchNotificationObserver = nil;
}
if (applicationDidBecomeActiveNotificationObserver != nil) {
NSNotificationCenter.defaultCenter().removeObserver(applicationDidBecomeActiveNotificationObserver!)
applicationDidBecomeActiveNotificationObserver = nil
}
}
}