PageRenderTime 169ms CodeModel.GetById 16ms app.highlight 144ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/update-engine/externals/gdata-objectivec-client/Source/OAuth2/GTMOAuth2SignIn.m

http://macfuse.googlecode.com/
Objective C | 952 lines | 658 code | 152 blank | 142 comment | 122 complexity | f202854e5545fa75cd407caa497cfa66 MD5 | raw file
  1/* Copyright (c) 2011 Google Inc.
  2 *
  3 * Licensed under the Apache License, Version 2.0 (the "License");
  4 * you may not use this file except in compliance with the License.
  5 * You may obtain a copy of the License at
  6 *
  7 *     http://www.apache.org/licenses/LICENSE-2.0
  8 *
  9 * Unless required by applicable law or agreed to in writing, software
 10 * distributed under the License is distributed on an "AS IS" BASIS,
 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12 * See the License for the specific language governing permissions and
 13 * limitations under the License.
 14 */
 15
 16#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
 17
 18#import "GTMOAuth2SignIn.h"
 19
 20// we'll default to timing out if the network becomes unreachable for more
 21// than 30 seconds when the sign-in page is displayed
 22static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0;
 23
 24// URI indicating an installed app is signing in. This is described at
 25//
 26// http://code.google.com/apis/accounts/docs/OAuth2.html#IA
 27//
 28NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob";
 29
 30
 31@interface GTMOAuth2SignIn ()
 32@property (assign) BOOL hasHandledCallback;
 33@property (retain) GTMHTTPFetcher *pendingFetcher;
 34#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
 35@property (nonatomic, retain, readwrite) NSDictionary *userProfile;
 36#endif
 37
 38- (void)invokeFinalCallbackWithError:(NSError *)error;
 39
 40- (BOOL)startWebRequest;
 41+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
 42                                      paramString:(NSString *)paramStr;
 43#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
 44- (void)addScopeForGoogleUserInfo;
 45- (void)fetchGoogleUserInfo;
 46#endif
 47- (void)finishSignInWithError:(NSError *)error;
 48
 49- (void)auth:(GTMOAuth2Authentication *)auth
 50finishedWithFetcher:(GTMHTTPFetcher *)fetcher
 51       error:(NSError *)error;
 52
 53#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
 54- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
 55   finishedWithData:(NSData *)data
 56              error:(NSError *)error;
 57+ (NSData *)decodeWebSafeBase64:(NSString *)base64Str;
 58- (void)updateGoogleUserInfoWithData:(NSData *)data;
 59#endif
 60
 61- (void)closeTheWindow;
 62
 63- (void)startReachabilityCheck;
 64- (void)stopReachabilityCheck;
 65- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
 66              changedFlags:(SCNetworkConnectionFlags)flags;
 67- (void)reachabilityTimerFired:(NSTimer *)timer;
 68@end
 69
 70@implementation GTMOAuth2SignIn
 71
 72@synthesize authentication = auth_;
 73
 74@synthesize authorizationURL = authorizationURL_;
 75@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_;
 76
 77@synthesize delegate = delegate_;
 78@synthesize webRequestSelector = webRequestSelector_;
 79@synthesize finishedSelector = finishedSelector_;
 80@synthesize hasHandledCallback = hasHandledCallback_;
 81@synthesize pendingFetcher = pendingFetcher_;
 82@synthesize userData = userData_;
 83
 84#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
 85@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_;
 86@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_;
 87@synthesize userProfile = userProfile_;
 88#endif
 89
 90@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_;
 91
 92#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
 93+ (NSURL *)googleAuthorizationURL {
 94  NSString *str = @"https://accounts.google.com/o/oauth2/auth";
 95  return [NSURL URLWithString:str];
 96}
 97
 98+ (NSURL *)googleTokenURL {
 99  NSString *str = @"https://accounts.google.com/o/oauth2/token";
100  return [NSURL URLWithString:str];
101}
102
103+ (NSURL *)googleRevocationURL {
104  NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke";
105  return [NSURL URLWithString:urlStr];
106}
107
108+ (NSURL *)googleUserInfoURL {
109  NSString *urlStr = @"https://www.googleapis.com/oauth2/v3/userinfo";
110  return [NSURL URLWithString:urlStr];
111}
112#endif
113
114+ (NSString *)nativeClientRedirectURI {
115  return kOOBString;
116}
117
118#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
119+ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
120                                                         clientID:(NSString *)clientID
121                                                     clientSecret:(NSString *)clientSecret {
122  NSString *redirectURI = [self nativeClientRedirectURI];
123  NSURL *tokenURL = [self googleTokenURL];
124
125  GTMOAuth2Authentication *auth;
126  auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
127                                                           tokenURL:tokenURL
128                                                        redirectURI:redirectURI
129                                                           clientID:clientID
130                                                       clientSecret:clientSecret];
131  auth.scope = scope;
132
133  return auth;
134}
135
136- (void)addScopeForGoogleUserInfo {
137  GTMOAuth2Authentication *auth = self.authentication;
138  if (self.shouldFetchGoogleUserEmail) {
139    NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email";
140    NSString *scope = auth.scope;
141    if ([scope rangeOfString:emailScope].location == NSNotFound) {
142      scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil];
143      auth.scope = scope;
144    }
145  }
146
147  if (self.shouldFetchGoogleUserProfile) {
148    NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile";
149    NSString *scope = auth.scope;
150    if ([scope rangeOfString:profileScope].location == NSNotFound) {
151      scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil];
152      auth.scope = scope;
153    }
154  }
155}
156#endif
157
158- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
159            authorizationURL:(NSURL *)authorizationURL
160                    delegate:(id)delegate
161          webRequestSelector:(SEL)webRequestSelector
162            finishedSelector:(SEL)finishedSelector {
163  // check the selectors on debug builds
164  GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector,
165    @encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0);
166  GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
167    @encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *),
168    @encode(NSError *), 0);
169
170  // designated initializer
171  self = [super init];
172  if (self) {
173    auth_ = [auth retain];
174    authorizationURL_ = [authorizationURL retain];
175    delegate_ = [delegate retain];
176    webRequestSelector_ = webRequestSelector;
177    finishedSelector_ = finishedSelector;
178
179    // for Google authentication, we want to automatically fetch user info
180#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
181    NSString *host = [authorizationURL host];
182    if ([host hasSuffix:@".google.com"]) {
183      shouldFetchGoogleUserEmail_ = YES;
184    }
185#endif
186
187    // default timeout for a lost internet connection while the server
188    // UI is displayed is 30 seconds
189    networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval;
190  }
191  return self;
192}
193
194- (void)dealloc {
195  [self stopReachabilityCheck];
196
197  [auth_ release];
198  [authorizationURL_ release];
199  [additionalAuthorizationParameters_ release];
200  [delegate_ release];
201  [pendingFetcher_ release];
202#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
203  [userProfile_ release];
204#endif
205  [userData_ release];
206
207  [super dealloc];
208}
209
210#pragma mark Sign-in Sequence Methods
211
212// stop any pending fetches, and close the window (but don't call the
213// delegate's finishedSelector)
214- (void)cancelSigningIn {
215  [self.pendingFetcher stopFetching];
216  self.pendingFetcher = nil;
217
218  [self.authentication stopAuthorization];
219
220  [self closeTheWindow];
221
222  [delegate_ autorelease];
223  delegate_ = nil;
224}
225
226//
227// This is the entry point to begin the sequence
228//  - display the authentication web page, and monitor redirects
229//  - exchange the code for an access token and a refresh token
230//  - for Google sign-in, fetch the user's email address
231//  - tell the delegate we're finished
232//
233- (BOOL)startSigningIn {
234  // For signing in to Google, append the scope for obtaining the authenticated
235  // user email and profile, as appropriate
236#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
237  [self addScopeForGoogleUserInfo];
238#endif
239
240  // start the authorization
241  return [self startWebRequest];
242}
243
244- (NSMutableDictionary *)parametersForWebRequest {
245  GTMOAuth2Authentication *auth = self.authentication;
246  NSString *clientID = auth.clientID;
247  NSString *redirectURI = auth.redirectURI;
248
249  BOOL hasClientID = ([clientID length] > 0);
250  BOOL hasRedirect = ([redirectURI length] > 0
251                      || redirectURI == [[self class] nativeClientRedirectURI]);
252  if (!hasClientID || !hasRedirect) {
253#if DEBUG
254    NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed");
255    NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed");
256#endif
257    return nil;
258  }
259
260  // invoke the UI controller's web request selector to display
261  // the authorization page
262
263  // add params to the authorization URL
264  NSString *scope = auth.scope;
265  if ([scope length] == 0) scope = nil;
266
267  NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
268                                     @"code", @"response_type",
269                                     clientID, @"client_id",
270                                     scope, @"scope", // scope may be nil
271                                     nil];
272  if (redirectURI) {
273    [paramsDict setObject:redirectURI forKey:@"redirect_uri"];
274  }
275  return paramsDict;
276}
277
278- (BOOL)startWebRequest {
279  NSMutableDictionary *paramsDict = [self parametersForWebRequest];
280
281  NSDictionary *additionalParams = self.additionalAuthorizationParameters;
282  if (additionalParams) {
283    [paramsDict addEntriesFromDictionary:additionalParams];
284  }
285
286  NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict];
287
288  NSURL *authorizationURL = self.authorizationURL;
289  NSMutableURLRequest *request;
290  request = [[self class] mutableURLRequestWithURL:authorizationURL
291                                       paramString:paramStr];
292
293  [delegate_ performSelector:self.webRequestSelector
294                  withObject:self
295                  withObject:request];
296
297  // at this point, we're waiting on the server-driven html UI, so
298  // we want notification if we lose connectivity to the web server
299  [self startReachabilityCheck];
300  return YES;
301}
302
303// utility for making a request from an old URL with some additional parameters
304+ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
305                                      paramString:(NSString *)paramStr {
306  if ([paramStr length] == 0) {
307    return [NSMutableURLRequest requestWithURL:oldURL];
308  }
309
310  NSString *query = [oldURL query];
311  if ([query length] > 0) {
312    query = [query stringByAppendingFormat:@"&%@", paramStr];
313  } else {
314    query = paramStr;
315  }
316
317  NSString *portStr = @"";
318  NSString *oldPort = [[oldURL port] stringValue];
319  if ([oldPort length] > 0) {
320    portStr = [@":" stringByAppendingString:oldPort];
321  }
322
323  NSString *qMark = [query length] > 0 ? @"?" : @"";
324  NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@",
325                         [oldURL scheme], [oldURL host], portStr,
326                         [oldURL path], qMark, query];
327  NSURL *newURL = [NSURL URLWithString:newURLStr];
328  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL];
329  return request;
330}
331
332// entry point for the window controller to tell us that the window
333// prematurely closed
334- (void)windowWasClosed {
335  [self stopReachabilityCheck];
336
337  NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
338                                       code:kGTMOAuth2ErrorWindowClosed
339                                   userInfo:nil];
340  [self invokeFinalCallbackWithError:error];
341}
342
343// internal method to tell the window controller to close the window
344- (void)closeTheWindow {
345  [self stopReachabilityCheck];
346
347  // a nil request means the window should be closed
348  [delegate_ performSelector:self.webRequestSelector
349                  withObject:self
350                  withObject:nil];
351}
352
353// entry point for the window controller to tell us what web page has been
354// requested
355//
356// When the request is for the callback URL, this method invokes
357// authCodeObtained and returns YES
358- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
359  // for Google's installed app sign-in protocol, we'll look for the
360  // end-of-sign-in indicator in the titleChanged: method below
361  NSString *redirectURI = self.authentication.redirectURI;
362  if (redirectURI == nil) return NO;
363
364  // when we're searching for the window title string, then we can ignore
365  // redirects
366  NSString *standardURI = [[self class] nativeClientRedirectURI];
367  if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;
368
369  // compare the redirectURI, which tells us when the web sign-in is done,
370  // to the actual redirection
371  NSURL *redirectURL = [NSURL URLWithString:redirectURI];
372  NSURL *requestURL = [redirectedRequest URL];
373
374  // avoid comparing to nil host and path values (such as when redirected to
375  // "about:blank")
376  NSString *requestHost = [requestURL host];
377  NSString *requestPath = [requestURL path];
378  BOOL isCallback;
379  if (requestHost && requestPath) {
380    isCallback = [[redirectURL host] isEqual:[requestURL host]]
381                 && [[redirectURL path] isEqual:[requestURL path]];
382  } else if (requestURL) {
383    // handle "about:blank"
384    isCallback = [redirectURL isEqual:requestURL];
385  } else {
386    isCallback = NO;
387  }
388
389  if (!isCallback) {
390    // tell the caller that this request is nothing interesting
391    return NO;
392  }
393
394  // we've reached the callback URL
395
396  // try to get the access code
397  if (!self.hasHandledCallback) {
398    NSString *responseStr = [[redirectedRequest URL] query];
399    [self.authentication setKeysForResponseString:responseStr];
400
401#if DEBUG
402    NSAssert([self.authentication.code length] > 0
403             || [self.authentication.errorString length] > 0,
404             @"response lacks auth code or error");
405#endif
406
407    [self authCodeObtained];
408  }
409  // tell the delegate that we did handle this request
410  return YES;
411}
412
413// entry point for the window controller to tell us when a new page title has
414// been loadded
415//
416// When the title indicates sign-in has completed, this method invokes
417// authCodeObtained and returns YES
418- (BOOL)titleChanged:(NSString *)title {
419  // return YES if the OAuth flow ending title was detected
420
421  // right now we're just looking for a parameter list following the last space
422  // in the title string, but hopefully we'll eventually get something better
423  // from the server to search for
424  NSRange paramsRange = [title rangeOfString:@" "
425                                     options:NSBackwardsSearch];
426  NSUInteger spaceIndex = paramsRange.location;
427  if (spaceIndex != NSNotFound) {
428    NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)];
429
430    NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr];
431
432    NSString *code = [dict objectForKey:@"code"];
433    NSString *error = [dict objectForKey:@"error"];
434    if ([code length] > 0 || [error length] > 0) {
435
436      if (!self.hasHandledCallback) {
437        [self.authentication setKeysForResponseDictionary:dict];
438
439        [self authCodeObtained];
440      }
441      return YES;
442    }
443  }
444  return NO;
445}
446
447- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage {
448  // We're ignoring these.
449  return NO;
450};
451
452// entry point for the window controller to tell us when a load has failed
453// in the webview
454//
455// if the initial authorization URL fails, bail out so the user doesn't
456// see an empty webview
457- (BOOL)loadFailedWithError:(NSError *)error {
458  NSURL *authorizationURL = self.authorizationURL;
459  NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6
460
461  BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]]
462    && [[failedURL path] isEqual:[authorizationURL path]];
463
464  if (isAuthURL) {
465    // We can assume that we have no pending fetchers, since we only
466    // handle failure to load the initial authorization URL
467    [self closeTheWindow];
468    [self invokeFinalCallbackWithError:error];
469    return YES;
470  }
471  return NO;
472}
473
474- (void)authCodeObtained {
475  // the callback page was requested, or the authenticate code was loaded
476  // into a page's title, so exchange the auth code for access & refresh tokens
477  // and tell the window to close
478
479  // avoid duplicate signals that the callback point has been reached
480  self.hasHandledCallback = YES;
481
482  // If the signin was request for exchanging an authentication token to a
483  // refresh token, there is no window to close.
484  if (self.webRequestSelector) {
485    [self closeTheWindow];
486  } else {
487    // For signing in to Google, append the scope for obtaining the
488    // authenticated user email and profile, as appropriate. This is usually
489    // done by the startSigningIn method, but this method is not called when
490    // exchanging an authentication token for a refresh token.
491#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
492    [self addScopeForGoogleUserInfo];
493#endif
494  }
495
496  NSError *error = nil;
497
498  GTMOAuth2Authentication *auth = self.authentication;
499  NSString *code = auth.code;
500  if ([code length] > 0) {
501    // exchange the code for a token
502    SEL sel = @selector(auth:finishedWithFetcher:error:);
503    GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self
504                                              didFinishSelector:sel];
505    if (fetcher == nil) {
506      error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
507                                  code:-1
508                              userInfo:nil];
509    } else {
510      self.pendingFetcher = fetcher;
511    }
512
513    // notify the app so it can put up a post-sign in, pre-token exchange UI
514    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
515    [nc postNotificationName:kGTMOAuth2UserSignedIn
516                      object:self
517                    userInfo:nil];
518  } else {
519    // the callback lacked an auth code
520    NSString *errStr = auth.errorString;
521    NSDictionary *userInfo = nil;
522    if ([errStr length] > 0) {
523      userInfo = [NSDictionary dictionaryWithObject:errStr
524                                             forKey:kGTMOAuth2ErrorMessageKey];
525    }
526
527    error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
528                                code:kGTMOAuth2ErrorAuthorizationFailed
529                            userInfo:userInfo];
530  }
531
532  if (error) {
533    [self finishSignInWithError:error];
534  }
535}
536
537- (void)auth:(GTMOAuth2Authentication *)auth
538finishedWithFetcher:(GTMHTTPFetcher *)fetcher
539       error:(NSError *)error {
540  self.pendingFetcher = nil;
541
542#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
543  if (error == nil
544      && (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile)
545      && [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
546    // fetch the user's information from the Google server
547    [self fetchGoogleUserInfo];
548  } else {
549    // we're not authorizing with Google, so we're done
550    [self finishSignInWithError:error];
551  }
552#else
553  [self finishSignInWithError:error];
554#endif
555}
556
557#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
558+ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth {
559  // create a fetcher for obtaining the user's email or profile
560  NSURL *infoURL = [[self class] googleUserInfoURL];
561  NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL];
562
563  if ([auth respondsToSelector:@selector(userAgent)]) {
564    NSString *userAgent = [auth userAgent];
565    [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
566  }
567  [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
568
569  GTMHTTPFetcher *fetcher;
570  id <GTMHTTPFetcherServiceProtocol> fetcherService = nil;
571  if ([auth respondsToSelector:@selector(fetcherService)]) {
572    fetcherService = auth.fetcherService;
573  };
574  if (fetcherService) {
575    fetcher = [fetcherService fetcherWithRequest:request];
576  } else {
577    fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
578  }
579  fetcher.authorizer = auth;
580  fetcher.retryEnabled = YES;
581  fetcher.maxRetryInterval = 15.0;
582#if !STRIP_GTM_FETCH_LOGGING
583  // The user email address is known at token refresh time, not during the initial code exchange.
584  NSString *userEmail = auth.userEmail;
585  NSString *forStr = userEmail ? [NSString stringWithFormat:@"for \"%@\"", userEmail] : @"";
586  [fetcher setCommentWithFormat:@"GTMOAuth2 user info %@", forStr];
587#endif
588  return fetcher;
589}
590
591- (void)fetchGoogleUserInfo {
592  if (!self.shouldFetchGoogleUserProfile) {
593    // If we only need email and user ID, not the full profile, and we have an
594    // id_token, it may have the email and user ID so we won't need to fetch
595    // them.
596    GTMOAuth2Authentication *auth = self.authentication;
597    NSString *idToken = [auth.parameters objectForKey:@"id_token"];
598    if ([idToken length] > 0) {
599      // The id_token has three dot-delimited parts. The second is the
600      // JSON profile.
601      //
602      // http://www.tbray.org/ongoing/When/201x/2013/04/04/ID-Tokens
603      NSArray *parts = [idToken componentsSeparatedByString:@"."];
604      if ([parts count] == 3) {
605        NSString *part2 = [parts objectAtIndex:1];
606        if ([part2 length] > 0) {
607          NSData *data = [[self class] decodeWebSafeBase64:part2];
608          if ([data length] > 0) {
609            [self updateGoogleUserInfoWithData:data];
610            if ([[auth userID] length] > 0 && [[auth userEmail] length] > 0) {
611              // We obtained user ID and email from the ID token.
612              [self finishSignInWithError:nil];
613              return;
614            }
615          }
616        }
617      }
618    }
619  }
620
621  // Fetch the email and profile from the userinfo endpoint.
622  GTMOAuth2Authentication *auth = self.authentication;
623  GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth];
624  [fetcher beginFetchWithDelegate:self
625                didFinishSelector:@selector(infoFetcher:finishedWithData:error:)];
626
627  self.pendingFetcher = fetcher;
628
629  [auth notifyFetchIsRunning:YES
630                     fetcher:fetcher
631                        type:kGTMOAuth2FetchTypeUserInfo];
632}
633
634- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
635   finishedWithData:(NSData *)data
636              error:(NSError *)error {
637  GTMOAuth2Authentication *auth = self.authentication;
638  [auth notifyFetchIsRunning:NO
639                     fetcher:fetcher
640                        type:nil];
641
642  self.pendingFetcher = nil;
643
644  if (error) {
645#if DEBUG
646    if (data) {
647      NSString *dataStr = [[[NSString alloc] initWithData:data
648                                                 encoding:NSUTF8StringEncoding] autorelease];
649      NSLog(@"infoFetcher error: %@\n%@", error, dataStr);
650    }
651#endif
652  } else {
653    // We have the authenticated user's info
654    [self updateGoogleUserInfoWithData:data];
655  }
656  [self finishSignInWithError:error];
657}
658
659- (void)updateGoogleUserInfoWithData:(NSData *)data {
660  if (!data) return;
661
662  GTMOAuth2Authentication *auth = self.authentication;
663  NSDictionary *profileDict = [[auth class] dictionaryWithJSONData:data];
664  if (profileDict) {
665    // Profile dictionary keys mostly conform to
666    // http://openid.net/specs/openid-connect-messages-1_0.html#StandardClaims
667
668    self.userProfile = profileDict;
669
670    // Save the ID into the auth object
671    NSString *subjectID = [profileDict objectForKey:@"sub"];
672    [auth setUserID:subjectID];
673
674    // Save the email into the auth object
675    NSString *email = [profileDict objectForKey:@"email"];
676    [auth setUserEmail:email];
677
678#if DEBUG
679    NSAssert([subjectID length] > 0 && [email length] > 0,
680             @"profile lacks userID or userEmail: %@", profileDict);
681#endif
682
683    // The email_verified key is a boolean NSNumber in the userinfo
684    // endpoint response, but it is a string like "true" in the id_token.
685    // We want to consistently save it as a string of the boolean value,
686    // like @"1".
687    id verified = [profileDict objectForKey:@"email_verified"];
688    if ([verified isKindOfClass:[NSString class]]) {
689      verified = [NSNumber numberWithBool:[verified boolValue]];
690    }
691
692    [auth setUserEmailIsVerified:[verified stringValue]];
693  }
694}
695
696#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
697
698- (void)finishSignInWithError:(NSError *)error {
699  [self invokeFinalCallbackWithError:error];
700}
701
702// convenience method for making the final call to our delegate
703- (void)invokeFinalCallbackWithError:(NSError *)error {
704  if (delegate_ && finishedSelector_) {
705    GTMOAuth2Authentication *auth = self.authentication;
706
707    NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_];
708    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
709    [invocation setSelector:finishedSelector_];
710    [invocation setTarget:delegate_];
711    [invocation setArgument:&self atIndex:2];
712    [invocation setArgument:&auth atIndex:3];
713    [invocation setArgument:&error atIndex:4];
714    [invocation invoke];
715  }
716
717  // we'll no longer send messages to the delegate
718  //
719  // we want to autorelease it rather than assign to the property in case
720  // the delegate is below us in the call stack
721  [delegate_ autorelease];
722  delegate_ = nil;
723}
724
725#pragma mark Reachability monitoring
726
727static void ReachabilityCallBack(SCNetworkReachabilityRef target,
728                                 SCNetworkConnectionFlags flags,
729                                 void *info) {
730  // pass the flags to the signIn object
731  GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info;
732
733  [signIn reachabilityTarget:target
734                changedFlags:flags];
735}
736
737- (void)startReachabilityCheck {
738  // the user may set the timeout to 0 to skip the reachability checking
739  // during display of the sign-in page
740  if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) {
741    return;
742  }
743
744  // create a reachability target from the authorization URL, add our callback,
745  // and schedule it on the run loop so we'll be notified if the network drops
746  NSURL *url = self.authorizationURL;
747  const char* host = [[url host] UTF8String];
748  reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault,
749                                                         host);
750  if (reachabilityRef_) {
751    BOOL isScheduled = NO;
752    SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL };
753
754    if (SCNetworkReachabilitySetCallback(reachabilityRef_,
755                                         ReachabilityCallBack, &ctx)) {
756      if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_,
757                                                   CFRunLoopGetCurrent(),
758                                                   kCFRunLoopDefaultMode)) {
759        isScheduled = YES;
760      }
761    }
762
763    if (!isScheduled) {
764      CFRelease(reachabilityRef_);
765      reachabilityRef_ = NULL;
766    }
767  }
768}
769
770- (void)destroyUnreachabilityTimer {
771  [networkLossTimer_ invalidate];
772  [networkLossTimer_ autorelease];
773  networkLossTimer_ = nil;
774}
775
776- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
777              changedFlags:(SCNetworkConnectionFlags)flags {
778  BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0
779    && (flags & kSCNetworkFlagsConnectionRequired) == 0;
780
781  if (isConnected) {
782    // server is again reachable
783    [self destroyUnreachabilityTimer];
784
785    if (hasNotifiedNetworkLoss_) {
786      // tell the user that the network has been found
787      NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
788      [nc postNotificationName:kGTMOAuth2NetworkFound
789                        object:self
790                      userInfo:nil];
791      hasNotifiedNetworkLoss_ = NO;
792    }
793  } else {
794    // the server has become unreachable; start the timer, if necessary
795    if (networkLossTimer_ == nil
796        && networkLossTimeoutInterval_ > 0
797        && !hasNotifiedNetworkLoss_) {
798      SEL sel = @selector(reachabilityTimerFired:);
799      networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_
800                                                            target:self
801                                                          selector:sel
802                                                          userInfo:nil
803                                                           repeats:NO] retain];
804    }
805  }
806}
807
808- (void)reachabilityTimerFired:(NSTimer *)timer {
809  // the user may call [[notification object] cancelSigningIn] to
810  // dismiss the sign-in
811  if (!hasNotifiedNetworkLoss_) {
812    NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
813    [nc postNotificationName:kGTMOAuth2NetworkLost
814                      object:self
815                    userInfo:nil];
816    hasNotifiedNetworkLoss_ = YES;
817  }
818
819  [self destroyUnreachabilityTimer];
820}
821
822- (void)stopReachabilityCheck {
823  [self destroyUnreachabilityTimer];
824
825  if (reachabilityRef_) {
826    SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_,
827                                               CFRunLoopGetCurrent(),
828                                               kCFRunLoopDefaultMode);
829    SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL);
830
831    CFRelease(reachabilityRef_);
832    reachabilityRef_ = NULL;
833  }
834}
835
836#pragma mark Token Revocation
837
838#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
839+ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
840  if (auth.refreshToken != nil
841      && auth.canAuthorize
842      && [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
843
844    // create a signed revocation request for this authentication object
845    NSURL *url = [self googleRevocationURL];
846    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
847    [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
848
849    NSString *token = auth.refreshToken;
850    NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token];
851    if (encoded != nil) {
852      NSString *body = [@"token=" stringByAppendingString:encoded];
853
854      [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
855      [request setHTTPMethod:@"POST"];
856
857      NSString *userAgent = [auth userAgent];
858      [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
859
860      // there's nothing to be done if revocation succeeds or fails
861      GTMHTTPFetcher *fetcher;
862      id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
863      if (fetcherService) {
864        fetcher = [fetcherService fetcherWithRequest:request];
865      } else {
866        fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
867      }
868      [fetcher setCommentWithFormat:@"GTMOAuth2 revoke token for %@", auth.userEmail];
869
870      // Use a completion handler fetch for better debugging, but only if we're
871      // guaranteed that blocks are available in the runtime
872#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \
873    (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000))
874      // Blocks are available
875      [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
876  #if DEBUG
877        if (error) {
878          NSString *errStr = [[[NSString alloc] initWithData:data
879                                                    encoding:NSUTF8StringEncoding] autorelease];
880          NSLog(@"revoke error: %@", errStr);
881        }
882  #endif // DEBUG
883      }];
884#else
885      // Blocks may not be available
886      [fetcher beginFetchWithDelegate:nil didFinishSelector:NULL];
887#endif
888    }
889  }
890  [auth reset];
891}
892
893
894// Based on Cyrus Najmabadi's elegent little encoder and decoder from
895// http://www.cocoadev.com/index.pl?BaseSixtyFour and on GTLBase64
896
897+ (NSData *)decodeWebSafeBase64:(NSString *)base64Str {
898  static char decodingTable[128];
899  static BOOL hasInited = NO;
900
901  if (!hasInited) {
902    char webSafeEncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
903    memset(decodingTable, 0, 128);
904    for (unsigned int i = 0; i < sizeof(webSafeEncodingTable); i++) {
905      decodingTable[(unsigned int) webSafeEncodingTable[i]] = (char)i;
906    }
907    hasInited = YES;
908  }
909
910  // The input string should be plain ASCII.
911  const char *cString = [base64Str cStringUsingEncoding:NSASCIIStringEncoding];
912  if (cString == nil) return nil;
913
914  NSInteger inputLength = (NSInteger)strlen(cString);
915  // Input length is not being restricted to multiples of 4.
916  if (inputLength == 0) return [NSData data];
917
918  while (inputLength > 0 && cString[inputLength - 1] == '=') {
919    inputLength--;
920  }
921
922  NSInteger outputLength = inputLength * 3 / 4;
923  NSMutableData* data = [NSMutableData dataWithLength:(NSUInteger)outputLength];
924  uint8_t *output = [data mutableBytes];
925
926  NSInteger inputPoint = 0;
927  NSInteger outputPoint = 0;
928  char *table = decodingTable;
929
930  while (inputPoint < inputLength - 1) {
931    int i0 = cString[inputPoint++];
932    int i1 = cString[inputPoint++];
933    int i2 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; // 'A' will decode to \0
934    int i3 = inputPoint < inputLength ? cString[inputPoint++] : 'A';
935
936    output[outputPoint++] = (uint8_t)((table[i0] << 2) | (table[i1] >> 4));
937    if (outputPoint < outputLength) {
938      output[outputPoint++] = (uint8_t)(((table[i1] & 0xF) << 4) | (table[i2] >> 2));
939    }
940    if (outputPoint < outputLength) {
941      output[outputPoint++] = (uint8_t)(((table[i2] & 0x3) << 6) | table[i3]);
942    }
943  }
944
945  return data;
946}
947
948#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
949
950@end
951
952#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES