forked from aerogear/aerogear-ios-oauth2
-
Notifications
You must be signed in to change notification settings - Fork 0
/
OAuth2Module.swift
283 lines (241 loc) · 12.3 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
/*
* 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 AeroGearHttp
import Foundation
import UIKit
public typealias SuccessType = AnyObject?->()
public typealias FailureType = NSError->()
public let AGAppLaunchedWithURLNotification = "AGAppLaunchedWithURLNotification"
public let AGAppDidBecomeActiveNotification = "AGAppDidBecomeActiveNotification"
let AGAuthzErrorDomain = "AGAuthzErrorDomain"
enum AuthorizationState {
case AuthorizationStatePendingExternalApproval
case AuthorizationStateApproved
case AuthorizationStateUnknown
}
public class OAuth2Module {
let config: Config
var httpAuthz: Http
public var http: Http {
get {
var headerFields: [String: String]?
if (self.isAuthorized()) {
headerFields = self.authorizationFields()
return Http(url: nil, sessionConfig: nil, headers: headerFields != nil ? headerFields! : [String: String]())
}
return Http()
}
}
var oauth2Session: OAuth2Session
var applicationLaunchNotificationObserver: NSObjectProtocol?
var applicationDidBecomeActiveNotificationObserver: NSObjectProtocol?
var state: AuthorizationState
// Default accountId, default to TrustedPersistantOAuth2Session
public required convenience init(config: Config) {
if (config.accountId != nil) {
self.init(config: config, accountId:config.accountId!, session: TrustedPersistantOAuth2Session(accountId: config.accountId!))
} else {
let accountId = "ACCOUNT_FOR_CLIENTID_\(config.clientId)"
self.init(config: config, accountId: accountId, session: TrustedPersistantOAuth2Session(accountId: accountId))
}
}
public required init(config: Config, accountId: String, session: OAuth2Session) {
self.config = config
// TODO use timeout config paramter
self.httpAuthz = Http(url: config.base, sessionConfig: NSURLSessionConfiguration.defaultSessionConfiguration())
self.oauth2Session = session
self.state = .AuthorizationStateUnknown
}
// MARK: Public API - To be overriden if necessary by OAuth2 specific adapter
public func requestAuthorizationCodeSuccess(success: SuccessType, failure: FailureType) {
let urlString = self.urlAsString();
let url = NSURL(string: urlString)
// 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, success: success, failure: failure)
})
// 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
UIApplication.sharedApplication().openURL(url);
}
public func refreshAccessTokenSuccess(success: SuccessType, failure: FailureType) {
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!
}
httpAuthz.baseURL = config.accessTokenEndpointURL
httpAuthz.POST(parameters: paramDict, success: { (responseObject: AnyObject?) -> Void in
if let unwrappedResponse = responseObject 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, expiration: exp)
success(unwrappedResponse["access_token"]);
}
}, failure: { (error: NSError) -> Void in
failure(error);
})
}
}
public func exchangeAuthorizationCodeForAccessToken(code: String, success: SuccessType, failure: FailureType) {
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
}
httpAuthz.baseURL = config.accessTokenEndpointURL
httpAuthz.POST(parameters: paramDict, success: {(responseObject: AnyObject?) -> () in
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
self.oauth2Session.saveAccessToken(accessToken, refreshToken: refreshToken, expiration: exp)
success(accessToken)
}
}, failure: {(error: NSError) -> () in
failure(error)
})
}
public func requestAccessSuccess(success: SuccessType, failure: FailureType) {
if (self.oauth2Session.accessToken != nil && self.oauth2Session.tokenIsNotExpired()) {
// we already have a valid access token, nothing more to be done
success(self.oauth2Session.accessToken!);
} else if (self.oauth2Session.refreshToken != nil) {
// need to refresh token
self.refreshAccessTokenSuccess(success, failure:failure);
} else {
// ask for authorization code and once obtained exchange code for access token
self.requestAuthorizationCodeSuccess(success, failure:failure);
}
}
public func revokeAccessSuccess(success: SuccessType, failure: FailureType) {
// return if not yet initialized
if (self.oauth2Session.accessToken == nil) {
return;
}
let paramDict:[String:String] = ["token":self.oauth2Session.accessToken!]
httpAuthz.baseURL = config.revokeTokenEndpointURL!
httpAuthz.POST(parameters: paramDict, success: { (param: AnyObject?) -> () in
self.oauth2Session.saveAccessToken()
success(param!)
}, failure: { (error: NSError) -> () in
failure(error)
})
}
// MARK: Internal Methods
func extractCode(notification: NSNotification, success: SuccessType, failure: FailureType) {
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!, success: success, failure: failure)
// update state
state = .AuthorizationStateApproved
} else {
failure(NSError(domain:AGAuthzErrorDomain, code:0, userInfo:["NSLocalizedDescriptionKey": "User cancelled authorization."]))
}
// 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
}
}
func authorizationFields() -> [String: String]? {
if (self.oauth2Session.accessToken == nil) {
return nil
} else {
return ["Authorization":"Bearer \(self.oauth2Session.accessToken!)"]
}
}
func isAuthorized() -> Bool {
return self.oauth2Session.accessToken != nil && self.oauth2Session.tokenIsNotExpired()
}
func urlAsString() -> String {
let scope = self.scope()
let urlRedirect = self.urlEncodeString(config.redirectURL)
let url = "\(config.authzEndpointURL.absoluteString!)?scope=\(scope)&redirect_uri=\(urlRedirect)&client_id=\(config.clientId)&response_type=code"
return url
}
func scope() -> String {
// Create a string to concatenate all scopes existing in the _scopes array.
var scopeString = ""
for scope in config.scopes {
scopeString += self.urlEncodeString(scope)
// If the current scope is other than the last one, then add the "+" sign to the string to separate the scopes.
if (scope != config.scopes.last) {
scopeString += "+"
}
}
return scopeString
}
func urlEncodeString(stringToURLEncode: String) -> String {
let encodedURL = CFURLCreateStringByAddingPercentEscapes(nil,
stringToURLEncode as NSString,
nil,
"!@#$%&*'();:=+,/?[]",
CFStringBuiltInEncodings.UTF8.toRaw())
return encodedURL as NSString
}
}