-
Notifications
You must be signed in to change notification settings - Fork 195
/
GTMAppAuthFetcherAuthorization.m
518 lines (443 loc) · 18.9 KB
/
GTMAppAuthFetcherAuthorization.m
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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
/*! @file GTMAppAuthFetcherAuthorization.m
@brief GTMAppAuth SDK
@copyright
Copyright 2016 Google Inc.
@copydetails
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 "GTMAppAuthFetcherAuthorization.h"
#import "AppAuth.h"
#define GTMOAuth2AssertValidSelector GTMBridgeAssertValidSelector
/*! @brief Provides a template implementation for init-family methods which have been marked as
NS_UNAVILABLE. Stops the compiler from giving a warning when it's the super class'
designated initializer, and gives callers useful feedback telling them what the
new designated initializer is.
@remarks Takes a SEL as a parameter instead of a string so that we get compiler warnings if the
designated initializer's signature changes.
@param designatedInitializer A SEL referencing the designated initializer.
*/
#define GTM_UNAVAILABLE_USE_INITIALIZER(designatedInitializer) { \
NSString *reason = [NSString stringWithFormat:@"Called: %@\nDesignated Initializer:%@", \
NSStringFromSelector(_cmd), \
NSStringFromSelector(designatedInitializer)]; \
@throw [NSException exceptionWithName:@"Attempt to call unavailable initializer." \
reason:reason \
userInfo:nil]; \
}
/*! @brief Key used to encode the @c authState property for @c NSSecureCoding.
*/
static NSString *const kAuthStateKey = @"authState";
/*! @brief Key used to encode the @c serviceProvider property for @c NSSecureCoding.
*/
static NSString *const kServiceProviderKey = @"serviceProvider";
/*! @brief Key used to encode the @c userID property for @c NSSecureCoding.
*/
static NSString *const kUserIDKey = @"userID";
/*! @brief Key used to encode the @c userEmail property for @c NSSecureCoding.
*/
static NSString *const kUserEmailKey = @"userEmail";
/*! @brief Key used to encode the @c userEmailIsVerified property for @c NSSecureCoding.
*/
static NSString *const kUserEmailIsVerifiedKey = @"userEmailIsVerified";
NSString *const GTMAppAuthFetcherAuthorizationErrorDomain =
@"kGTMAppAuthFetcherAuthorizationErrorDomain";
NSString *const GTMAppAuthFetcherAuthorizationErrorRequestKey = @"request";
/*! @brief Internal wrapper class for requests needing authorization and their callbacks.
@discusssion Used to abstract away the detail of whether a callback or block is used.
*/
@interface GTMAppAuthFetcherAuthorizationArgs : NSObject
/*! @brief The request to authorize.
* @discussion Not copied, as we are mutating the request.
*/
@property (nonatomic, strong) NSMutableURLRequest *request;
/*! @brief The delegate on which @c selector is called on completion.
*/
@property (nonatomic, weak) id delegate;
/*! @brief The selector called on the @c delegate object on completion.
*/
@property (nonatomic) SEL selector;
/*! @brief The completion block when the block option was used.
*/
@property (nonatomic, strong) GTMAppAuthFetcherAuthorizationCompletion completionHandler;
/*! @brief The error that happened during token refresh (if any).
*/
@property (nonatomic, strong) NSError *error;
+ (GTMAppAuthFetcherAuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
delegate:(id)delegate
selector:(SEL)selector
completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)completionHandler;
@end
@implementation GTMAppAuthFetcherAuthorizationArgs
@synthesize request = _request;
@synthesize delegate = _delegate;
@synthesize selector = _selector;
@synthesize completionHandler = _completionHandler;
@synthesize error = _error;
+ (GTMAppAuthFetcherAuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
delegate:(id)delegate
selector:(SEL)selector
completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)completionHandler {
GTMAppAuthFetcherAuthorizationArgs *obj;
obj = [[GTMAppAuthFetcherAuthorizationArgs alloc] init];
obj.request = req;
obj.delegate = delegate;
obj.selector = selector;
obj.completionHandler = completionHandler;
return obj;
}
@end
@implementation GTMAppAuthFetcherAuthorization {
/*! @brief Array of requests pending authorization headers.
*/
NSMutableArray<GTMAppAuthFetcherAuthorizationArgs *> *_authorizationQueue;
}
@synthesize authState = _authState;
@synthesize serviceProvider = _serviceProvider;
@synthesize userID = _userID;
@synthesize userEmailIsVerified = _userEmailIsVerified;
// GTMFetcherAuthorizationProtocol doesn't specify atomic/nonatomic for these properties.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wimplicit-atomic-properties"
@synthesize userEmail = _userEmail;
@synthesize shouldAuthorizeAllRequests = _shouldAuthorizeAllRequests;
@synthesize fetcherService = _fetcherService;
#pragma clang diagnostic pop
#pragma mark - Initializers
// Ignore warning about not calling the designated initializer.
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wobjc-designated-initializers"
- (instancetype)init
GTM_UNAVAILABLE_USE_INITIALIZER(@selector(initWithAuthState:));
#pragma clang diagnostic pop
- (instancetype)initWithAuthState:(OIDAuthState *)authState {
return [self initWithAuthState:authState
serviceProvider:nil
userID:nil
userEmail:nil
userEmailIsVerified:nil];
}
- (instancetype)initWithAuthState:(OIDAuthState *)authState
serviceProvider:(nullable NSString *)serviceProvider
userID:(nullable NSString *)userID
userEmail:(nullable NSString *)userEmail
userEmailIsVerified:(nullable NSString *)userEmailIsVerified {
self = [super init];
if (self) {
_authState = authState;
_authorizationQueue = [[NSMutableArray alloc] init];
_serviceProvider = [serviceProvider copy];
_userID = [userID copy];
_userEmail = [userEmail copy];
_userEmailIsVerified = [userEmailIsVerified copy];
// Decodes the ID Token locally to extract the email address.
NSString *idToken = _authState.lastTokenResponse.idToken
? : _authState.lastAuthorizationResponse.idToken;
if (idToken) {
NSDictionary *claimsDictionary = [[self class] extractIDTokenClaimsNoVerification:idToken];
if (claimsDictionary) {
_userEmail = (NSString *)[claimsDictionary[@"email"] copy];
_userEmailIsVerified = [(NSNumber *)claimsDictionary[@"email_verified"] stringValue];
_userID = [claimsDictionary[@"sub"] copy];
}
}
}
return self;
}
#pragma mark - NSSecureCoding
+ (BOOL)supportsSecureCoding {
return YES;
}
- (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
OIDAuthState *authState =
[aDecoder decodeObjectOfClass:[OIDAuthState class] forKey:kAuthStateKey];
NSString *serviceProvider =
[aDecoder decodeObjectOfClass:[NSString class] forKey:kServiceProviderKey];
NSString *userID = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserIDKey];
NSString *userEmail = [aDecoder decodeObjectOfClass:[NSString class] forKey:kUserEmailKey];
NSString *userEmailIsVerified =
[aDecoder decodeObjectOfClass:[NSString class] forKey:kUserEmailIsVerifiedKey];
self = [self initWithAuthState:authState
serviceProvider:serviceProvider
userID:userID
userEmail:userEmail
userEmailIsVerified:userEmailIsVerified];
return self;
}
- (void)encodeWithCoder:(NSCoder *)aCoder {
[aCoder encodeObject:_authState forKey:kAuthStateKey];
[aCoder encodeObject:_serviceProvider forKey:kServiceProviderKey];
[aCoder encodeObject:_userID forKey:kUserIDKey];
[aCoder encodeObject:_userEmail forKey:kUserEmailKey];
[aCoder encodeObject:_userEmailIsVerified forKey:kUserEmailIsVerifiedKey];
}
# pragma mark - Convenience
#if !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT
+ (OIDServiceConfiguration *)configurationForGoogle {
NSURL *authorizationEndpoint =
[NSURL URLWithString:@"https://accounts.google.com/o/oauth2/v2/auth"];
NSURL *tokenEndpoint =
[NSURL URLWithString:@"https://www.googleapis.com/oauth2/v4/token"];
OIDServiceConfiguration *configuration =
[[OIDServiceConfiguration alloc] initWithAuthorizationEndpoint:authorizationEndpoint
tokenEndpoint:tokenEndpoint];
return configuration;
}
#endif // !GTM_APPAUTH_SKIP_GOOGLE_SUPPORT
# pragma mark - ID Token extraction
+ (nullable NSDictionary *)extractIDTokenClaimsNoVerification:(NSString *)idToken {
NSArray *sections = [idToken componentsSeparatedByString:@"."];
if (sections.count > 1) {
// Gets the JWT payload section.
NSMutableString *body = [sections[1] mutableCopy];
// Converts base64url to base64.
NSRange range = NSMakeRange(0, body.length);
[body replaceOccurrencesOfString:@"-" withString:@"+" options:NSLiteralSearch range:range];
[body replaceOccurrencesOfString:@"_" withString:@"/" options:NSLiteralSearch range:range];
// Converts base64 no padding to base64 with padding
while (body.length % 4 != 0) {
[body appendString:@"="];
}
// Decodes base64 string.
NSData *decodedData = [[NSData alloc] initWithBase64EncodedString:body options:0];
// Parses JSON.
NSError *error;
id object = [NSJSONSerialization JSONObjectWithData:decodedData options:0 error:&error];
if (error) {
NSLog(@"Error %@ parsing token payload %@", error, body);
}
if ([object isKindOfClass:[NSDictionary class]]) {
return (NSDictionary *)object;
}
}
return nil;
}
#pragma mark - GTMFetcherAuthorizationProtocol
#pragma mark Authorizing Requests
/*! @brief Authorizing with a completion block.
*/
- (void)authorizeRequest:(NSMutableURLRequest *)request
completionHandler:(GTMAppAuthFetcherAuthorizationCompletion)handler {
GTMAppAuthFetcherAuthorizationArgs *args =
[GTMAppAuthFetcherAuthorizationArgs argsWithRequest:request
delegate:nil
selector:NULL
completionHandler:handler];
[self authorizeRequestArgs:args];
}
/*! @brief Authorizing with a callback selector.
@discussion Selector has the signature:
- (void)authentication:(GTMAppAuthFetcherAuthorization *)auth
request:(NSMutableURLRequest *)request
finishedWithError:(NSError *)error;
*/
- (void)authorizeRequest:(NSMutableURLRequest *)request
delegate:(id)delegate
didFinishSelector:(SEL)sel {
GTMOAuth2AssertValidSelector(delegate, sel,
@encode(GTMAppAuthFetcherAuthorization *),
@encode(NSMutableURLRequest *),
@encode(NSError *), 0);
GTMAppAuthFetcherAuthorizationArgs *args;
args = [GTMAppAuthFetcherAuthorizationArgs argsWithRequest:request
delegate:delegate
selector:sel
completionHandler:nil];
[self authorizeRequestArgs:args];
}
/*! @brief Internal routine common to delegate and block invocations to queue requests while
fresh tokens are obtained.
*/
- (void)authorizeRequestArgs:(GTMAppAuthFetcherAuthorizationArgs *)args {
// Adds requests to queue.
@synchronized(_authorizationQueue) {
[_authorizationQueue addObject:args];
}
// Obtains fresh tokens from AppAuth.
[_authState performActionWithFreshTokens:^(NSString *_Nullable accessToken,
NSString *_Nullable idToken,
NSError *_Nullable error) {
// Processes queue.
@synchronized(_authorizationQueue) {
for (GTMAppAuthFetcherAuthorizationArgs *fetcherArgs in _authorizationQueue) {
[self authorizeRequestImmediateArgs:fetcherArgs accessToken:accessToken error:error];
}
[_authorizationQueue removeAllObjects];
}
}];
}
/*! @brief Adds authorization headers to the given request, using the supplied access token, or
handles the error.
@param args The request argument group to authorize.
@param accessToken A currently valid access token.
@param error If accessToken is nil, the error which caused the token to be unavailable.
@return YES if the request was authorized with a valid access token.
*/
- (BOOL)authorizeRequestImmediateArgs:(GTMAppAuthFetcherAuthorizationArgs *)args
accessToken:(NSString *)accessToken
error:(NSError *)error {
// This authorization entry point never attempts to refresh the access token,
// but does call the completion routine
NSMutableURLRequest *request = args.request;
NSURL *requestURL = [request URL];
NSString *scheme = [requestURL scheme];
BOOL isAuthorizableRequest =
!requestURL
|| (scheme && [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame)
|| [requestURL isFileURL]
|| self.shouldAuthorizeAllRequests;
if (!isAuthorizableRequest) {
// Request is not https, a local file, or nil, so may be insecure
//
// The NSError will be created below
#if DEBUG
NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request);
#endif
}
// Get the access token.
if (isAuthorizableRequest && accessToken && accessToken.length > 0) {
if (request) {
// Adds the authorization header to the request.
NSString *value = [NSString stringWithFormat:@"%@ %@", @"Bearer", accessToken];
[request setValue:value forHTTPHeaderField:@"Authorization"];
}
// We've authorized the request, even if the previous refresh
// failed with an error
args.error = nil;
} else {
NSMutableDictionary *userInfo = [error.userInfo mutableCopy];
if (!userInfo) {
userInfo = [[NSMutableDictionary alloc] init];
}
if (request) {
userInfo[GTMAppAuthFetcherAuthorizationErrorRequestKey] = request;
}
if (!isAuthorizableRequest || !error) {
args.error = [NSError errorWithDomain:GTMAppAuthFetcherAuthorizationErrorDomain
code:GTMAppAuthFetcherAuthorizationErrorUnauthorizableRequest
userInfo:userInfo];
} else {
// Passes through error domain & code from AppAuth, with additional userInfo args.
args.error = [NSError errorWithDomain:error.domain
code:error.code
userInfo:userInfo];
}
}
// Invoke any callbacks on the proper thread
if (args.delegate || args.completionHandler) {
// If the fetcher service provides a callback queue, we'll use that
// (or if it's nil, we'll use the main thread) for callbacks.
dispatch_queue_t callbackQueue = self.fetcherService.callbackQueue;
if (!callbackQueue) {
callbackQueue = dispatch_get_main_queue();
}
dispatch_async(callbackQueue, ^{
[self invokeCallbackArgs:args];
});
}
BOOL didAuth = (args.error == nil);
return didAuth;
}
/*! @brief Invokes the callback for the given authorization argument group.
@param args The request argument group to invoke following authorization or error.
*/
- (void)invokeCallbackArgs:(GTMAppAuthFetcherAuthorizationArgs *)args {
NSError *error = args.error;
id delegate = args.delegate;
SEL sel = args.selector;
// If the selector callback method exists, invokes the selector.
if (delegate && sel) {
NSMutableURLRequest *request = args.request;
NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
[invocation setSelector:sel];
[invocation setTarget:delegate];
GTMAppAuthFetcherAuthorization *authorization = self;
[invocation setArgument:&authorization atIndex:2];
[invocation setArgument:&request atIndex:3];
[invocation setArgument:&error atIndex:4];
[invocation invoke];
}
// If a callback block exists, executes the block.
id handler = args.completionHandler;
if (handler) {
void (^authCompletionBlock)(NSError *) = handler;
authCompletionBlock(error);
}
}
/*! @brief Removes all pending requests from the authorization queue.
*/
- (void)stopAuthorization {
@synchronized(_authorizationQueue) {
[_authorizationQueue removeAllObjects];
}
}
/*! @brief Attempts to remove a specific pending requests from the authorization queue.
@discussion Has no effect if the authorization already occurred.
*/
- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
@synchronized(_authorizationQueue) {
NSUInteger argIndex = 0;
BOOL found = NO;
for (GTMAppAuthFetcherAuthorizationArgs *args in _authorizationQueue) {
// Checks pointer equality with given request, don't want to match equivalent requests.
if ([args request] == request) {
found = YES;
break;
}
argIndex++;
}
if (found) {
[_authorizationQueue removeObjectAtIndex:argIndex];
// If the queue is now empty, go ahead and stop the fetcher.
if (_authorizationQueue.count == 0) {
[self stopAuthorization];
}
}
}
}
/*! @brief Returns YES if the given requests is in the pending authorization queue.
*/
- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
BOOL wasFound = NO;
@synchronized(_authorizationQueue) {
for (GTMAppAuthFetcherAuthorizationArgs *args in _authorizationQueue) {
// Checks pointer equality with given request, don't want to match equivalent requests.
if ([args request] == request) {
wasFound = YES;
break;
}
}
}
return wasFound;
}
/*! @brief Returns YES if given request has an Authorization header.
*/
- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"];
return (authStr.length > 0);
}
/*! @brief Returns YES if the authorization state is currently valid.
@discussion Note that the state can become invalid immediately due to an error on token refresh.
*/
- (BOOL)canAuthorize {
return [_authState isAuthorized];
}
/*! @brief Forces a token refresh the next time a request is queued for authorization.
*/
- (BOOL)primeForRefresh {
if (_authState.refreshToken == nil) {
// Cannot refresh without a refresh token
return NO;
}
[_authState setNeedsTokenRefresh];
return YES;
}
@end