aboutsummaryrefslogtreecommitdiffhomepage
path: root/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m
diff options
context:
space:
mode:
Diffstat (limited to 'example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m')
-rw-r--r--example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m939
1 files changed, 939 insertions, 0 deletions
diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m
new file mode 100644
index 00000000..d9df97e4
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m
@@ -0,0 +1,939 @@
+/* Copyright (c) 2011 Google Inc.
+ *
+ * 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.
+ */
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#define GTMOAUTH2SIGNIN_DEFINE_GLOBALS 1
+#import "GTMOAuth2SignIn.h"
+
+// we'll default to timing out if the network becomes unreachable for more
+// than 30 seconds when the sign-in page is displayed
+static const NSTimeInterval kDefaultNetworkLossTimeoutInterval = 30.0;
+
+// URI indicating an installed app is signing in. This is described at
+//
+// http://code.google.com/apis/accounts/docs/OAuth2.html#IA
+//
+NSString *const kOOBString = @"urn:ietf:wg:oauth:2.0:oob";
+
+
+@interface GTMOAuth2SignIn ()
+@property (assign) BOOL hasHandledCallback;
+@property (retain) GTMHTTPFetcher *pendingFetcher;
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+@property (nonatomic, retain, readwrite) NSDictionary *userProfile;
+#endif
+
+- (void)invokeFinalCallbackWithError:(NSError *)error;
+
+- (BOOL)startWebRequest;
++ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
+ paramString:(NSString *)paramStr;
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+- (void)addScopeForGoogleUserInfo;
+- (void)fetchGoogleUserInfo;
+#endif
+- (void)finishSignInWithError:(NSError *)error;
+
+- (void)auth:(GTMOAuth2Authentication *)auth
+finishedWithFetcher:(GTMHTTPFetcher *)fetcher
+ error:(NSError *)error;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error;
++ (NSData *)decodeWebSafeBase64:(NSString *)base64Str;
+#endif
+
+- (void)closeTheWindow;
+
+- (void)startReachabilityCheck;
+- (void)stopReachabilityCheck;
+- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
+ changedFlags:(SCNetworkConnectionFlags)flags;
+- (void)reachabilityTimerFired:(NSTimer *)timer;
+@end
+
+@implementation GTMOAuth2SignIn
+
+@synthesize authentication = auth_;
+
+@synthesize authorizationURL = authorizationURL_;
+@synthesize additionalAuthorizationParameters = additionalAuthorizationParameters_;
+
+@synthesize delegate = delegate_;
+@synthesize webRequestSelector = webRequestSelector_;
+@synthesize finishedSelector = finishedSelector_;
+@synthesize hasHandledCallback = hasHandledCallback_;
+@synthesize pendingFetcher = pendingFetcher_;
+@synthesize userData = userData_;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+@synthesize shouldFetchGoogleUserEmail = shouldFetchGoogleUserEmail_;
+@synthesize shouldFetchGoogleUserProfile = shouldFetchGoogleUserProfile_;
+@synthesize userProfile = userProfile_;
+#endif
+
+@synthesize networkLossTimeoutInterval = networkLossTimeoutInterval_;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (NSURL *)googleAuthorizationURL {
+ NSString *str = @"https://accounts.google.com/o/oauth2/auth";
+ return [NSURL URLWithString:str];
+}
+
++ (NSURL *)googleTokenURL {
+ NSString *str = @"https://accounts.google.com/o/oauth2/token";
+ return [NSURL URLWithString:str];
+}
+
++ (NSURL *)googleRevocationURL {
+ NSString *urlStr = @"https://accounts.google.com/o/oauth2/revoke";
+ return [NSURL URLWithString:urlStr];
+}
+
++ (NSURL *)googleUserInfoURL {
+ NSString *urlStr = @"https://www.googleapis.com/oauth2/v1/userinfo";
+ return [NSURL URLWithString:urlStr];
+}
+#endif
+
++ (NSString *)nativeClientRedirectURI {
+ return kOOBString;
+}
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret {
+ NSString *redirectURI = [self nativeClientRedirectURI];
+ NSURL *tokenURL = [self googleTokenURL];
+
+ GTMOAuth2Authentication *auth;
+ auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
+ tokenURL:tokenURL
+ redirectURI:redirectURI
+ clientID:clientID
+ clientSecret:clientSecret];
+ auth.scope = scope;
+
+ return auth;
+}
+
+- (void)addScopeForGoogleUserInfo {
+ GTMOAuth2Authentication *auth = self.authentication;
+ if (self.shouldFetchGoogleUserEmail) {
+ NSString *const emailScope = @"https://www.googleapis.com/auth/userinfo.email";
+ NSString *scope = auth.scope;
+ if ([scope rangeOfString:emailScope].location == NSNotFound) {
+ scope = [GTMOAuth2Authentication scopeWithStrings:scope, emailScope, nil];
+ auth.scope = scope;
+ }
+ }
+
+ if (self.shouldFetchGoogleUserProfile) {
+ NSString *const profileScope = @"https://www.googleapis.com/auth/userinfo.profile";
+ NSString *scope = auth.scope;
+ if ([scope rangeOfString:profileScope].location == NSNotFound) {
+ scope = [GTMOAuth2Authentication scopeWithStrings:scope, profileScope, nil];
+ auth.scope = scope;
+ }
+ }
+}
+#endif
+
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ delegate:(id)delegate
+ webRequestSelector:(SEL)webRequestSelector
+ finishedSelector:(SEL)finishedSelector {
+ // check the selectors on debug builds
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, webRequestSelector,
+ @encode(GTMOAuth2SignIn *), @encode(NSURLRequest *), 0);
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
+ @encode(GTMOAuth2SignIn *), @encode(GTMOAuth2Authentication *),
+ @encode(NSError *), 0);
+
+ // designated initializer
+ self = [super init];
+ if (self) {
+ auth_ = [auth retain];
+ authorizationURL_ = [authorizationURL retain];
+ delegate_ = [delegate retain];
+ webRequestSelector_ = webRequestSelector;
+ finishedSelector_ = finishedSelector;
+
+ // for Google authentication, we want to automatically fetch user info
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ NSString *host = [authorizationURL host];
+ if ([host hasSuffix:@".google.com"]) {
+ shouldFetchGoogleUserEmail_ = YES;
+ }
+#endif
+
+ // default timeout for a lost internet connection while the server
+ // UI is displayed is 30 seconds
+ networkLossTimeoutInterval_ = kDefaultNetworkLossTimeoutInterval;
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [self stopReachabilityCheck];
+
+ [auth_ release];
+ [authorizationURL_ release];
+ [additionalAuthorizationParameters_ release];
+ [delegate_ release];
+ [pendingFetcher_ release];
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ [userProfile_ release];
+#endif
+ [userData_ release];
+
+ [super dealloc];
+}
+
+#pragma mark Sign-in Sequence Methods
+
+// stop any pending fetches, and close the window (but don't call the
+// delegate's finishedSelector)
+- (void)cancelSigningIn {
+ [self.pendingFetcher stopFetching];
+ self.pendingFetcher = nil;
+
+ [self.authentication stopAuthorization];
+
+ [self closeTheWindow];
+
+ [delegate_ autorelease];
+ delegate_ = nil;
+}
+
+//
+// This is the entry point to begin the sequence
+// - display the authentication web page, and monitor redirects
+// - exchange the code for an access token and a refresh token
+// - for Google sign-in, fetch the user's email address
+// - tell the delegate we're finished
+//
+- (BOOL)startSigningIn {
+ // For signing in to Google, append the scope for obtaining the authenticated
+ // user email and profile, as appropriate
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ [self addScopeForGoogleUserInfo];
+#endif
+
+ // start the authorization
+ return [self startWebRequest];
+}
+
+- (NSMutableDictionary *)parametersForWebRequest {
+ GTMOAuth2Authentication *auth = self.authentication;
+ NSString *clientID = auth.clientID;
+ NSString *redirectURI = auth.redirectURI;
+
+ BOOL hasClientID = ([clientID length] > 0);
+ BOOL hasRedirect = ([redirectURI length] > 0
+ || redirectURI == [[self class] nativeClientRedirectURI]);
+ if (!hasClientID || !hasRedirect) {
+#if DEBUG
+ NSAssert(hasClientID, @"GTMOAuth2SignIn: clientID needed");
+ NSAssert(hasRedirect, @"GTMOAuth2SignIn: redirectURI needed");
+#endif
+ return NO;
+ }
+
+ // invoke the UI controller's web request selector to display
+ // the authorization page
+
+ // add params to the authorization URL
+ NSString *scope = auth.scope;
+ if ([scope length] == 0) scope = nil;
+
+ NSMutableDictionary *paramsDict = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ @"code", @"response_type",
+ clientID, @"client_id",
+ scope, @"scope", // scope may be nil
+ nil];
+ if (redirectURI) {
+ [paramsDict setObject:redirectURI forKey:@"redirect_uri"];
+ }
+ return paramsDict;
+}
+
+- (BOOL)startWebRequest {
+ NSMutableDictionary *paramsDict = [self parametersForWebRequest];
+
+ NSDictionary *additionalParams = self.additionalAuthorizationParameters;
+ if (additionalParams) {
+ [paramsDict addEntriesFromDictionary:additionalParams];
+ }
+
+ NSString *paramStr = [GTMOAuth2Authentication encodedQueryParametersForDictionary:paramsDict];
+
+ NSURL *authorizationURL = self.authorizationURL;
+ NSMutableURLRequest *request;
+ request = [[self class] mutableURLRequestWithURL:authorizationURL
+ paramString:paramStr];
+
+ [delegate_ performSelector:self.webRequestSelector
+ withObject:self
+ withObject:request];
+
+ // at this point, we're waiting on the server-driven html UI, so
+ // we want notification if we lose connectivity to the web server
+ [self startReachabilityCheck];
+ return YES;
+}
+
+// utility for making a request from an old URL with some additional parameters
++ (NSMutableURLRequest *)mutableURLRequestWithURL:(NSURL *)oldURL
+ paramString:(NSString *)paramStr {
+ if ([paramStr length] == 0) {
+ return [NSMutableURLRequest requestWithURL:oldURL];
+ }
+
+ NSString *query = [oldURL query];
+ if ([query length] > 0) {
+ query = [query stringByAppendingFormat:@"&%@", paramStr];
+ } else {
+ query = paramStr;
+ }
+
+ NSString *portStr = @"";
+ NSString *oldPort = [[oldURL port] stringValue];
+ if ([oldPort length] > 0) {
+ portStr = [@":" stringByAppendingString:oldPort];
+ }
+
+ NSString *qMark = [query length] > 0 ? @"?" : @"";
+ NSString *newURLStr = [NSString stringWithFormat:@"%@://%@%@%@%@%@",
+ [oldURL scheme], [oldURL host], portStr,
+ [oldURL path], qMark, query];
+ NSURL *newURL = [NSURL URLWithString:newURLStr];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:newURL];
+ return request;
+}
+
+// entry point for the window controller to tell us that the window
+// prematurely closed
+- (void)windowWasClosed {
+ [self stopReachabilityCheck];
+
+ NSError *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
+ code:kGTMOAuth2ErrorWindowClosed
+ userInfo:nil];
+ [self invokeFinalCallbackWithError:error];
+}
+
+// internal method to tell the window controller to close the window
+- (void)closeTheWindow {
+ [self stopReachabilityCheck];
+
+ // a nil request means the window should be closed
+ [delegate_ performSelector:self.webRequestSelector
+ withObject:self
+ withObject:nil];
+}
+
+// entry point for the window controller to tell us what web page has been
+// requested
+//
+// When the request is for the callback URL, this method invokes
+// authCodeObtained and returns YES
+- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest {
+ // for Google's installed app sign-in protocol, we'll look for the
+ // end-of-sign-in indicator in the titleChanged: method below
+ NSString *redirectURI = self.authentication.redirectURI;
+ if (redirectURI == nil) return NO;
+
+ // when we're searching for the window title string, then we can ignore
+ // redirects
+ NSString *standardURI = [[self class] nativeClientRedirectURI];
+ if (standardURI != nil && [redirectURI isEqual:standardURI]) return NO;
+
+ // compare the redirectURI, which tells us when the web sign-in is done,
+ // to the actual redirection
+ NSURL *redirectURL = [NSURL URLWithString:redirectURI];
+ NSURL *requestURL = [redirectedRequest URL];
+
+ // avoid comparing to nil host and path values (such as when redirected to
+ // "about:blank")
+ NSString *requestHost = [requestURL host];
+ NSString *requestPath = [requestURL path];
+ BOOL isCallback;
+ if (requestHost && requestPath) {
+ isCallback = [[redirectURL host] isEqual:[requestURL host]]
+ && [[redirectURL path] isEqual:[requestURL path]];
+ } else if (requestURL) {
+ // handle "about:blank"
+ isCallback = [redirectURL isEqual:requestURL];
+ } else {
+ isCallback = NO;
+ }
+
+ if (!isCallback) {
+ // tell the caller that this request is nothing interesting
+ return NO;
+ }
+
+ // we've reached the callback URL
+
+ // try to get the access code
+ if (!self.hasHandledCallback) {
+ NSString *responseStr = [[redirectedRequest URL] query];
+ [self.authentication setKeysForResponseString:responseStr];
+
+#if DEBUG
+ NSAssert([self.authentication.code length] > 0
+ || [self.authentication.errorString length] > 0,
+ @"response lacks auth code or error");
+#endif
+
+ [self authCodeObtained];
+ }
+ // tell the delegate that we did handle this request
+ return YES;
+}
+
+// entry point for the window controller to tell us when a new page title has
+// been loadded
+//
+// When the title indicates sign-in has completed, this method invokes
+// authCodeObtained and returns YES
+- (BOOL)titleChanged:(NSString *)title {
+ // return YES if the OAuth flow ending title was detected
+
+ // right now we're just looking for a parameter list following the last space
+ // in the title string, but hopefully we'll eventually get something better
+ // from the server to search for
+ NSRange paramsRange = [title rangeOfString:@" "
+ options:NSBackwardsSearch];
+ NSUInteger spaceIndex = paramsRange.location;
+ if (spaceIndex != NSNotFound) {
+ NSString *responseStr = [title substringFromIndex:(spaceIndex + 1)];
+
+ NSDictionary *dict = [GTMOAuth2Authentication dictionaryWithResponseString:responseStr];
+
+ NSString *code = [dict objectForKey:@"code"];
+ NSString *error = [dict objectForKey:@"error"];
+ if ([code length] > 0 || [error length] > 0) {
+
+ if (!self.hasHandledCallback) {
+ [self.authentication setKeysForResponseDictionary:dict];
+
+ [self authCodeObtained];
+ }
+ return YES;
+ }
+ }
+ return NO;
+}
+
+- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage {
+ // We're ignoring these.
+ return NO;
+};
+
+// entry point for the window controller to tell us when a load has failed
+// in the webview
+//
+// if the initial authorization URL fails, bail out so the user doesn't
+// see an empty webview
+- (BOOL)loadFailedWithError:(NSError *)error {
+ NSURL *authorizationURL = self.authorizationURL;
+ NSURL *failedURL = [[error userInfo] valueForKey:@"NSErrorFailingURLKey"]; // NSURLErrorFailingURLErrorKey defined in 10.6
+
+ BOOL isAuthURL = [[failedURL host] isEqual:[authorizationURL host]]
+ && [[failedURL path] isEqual:[authorizationURL path]];
+
+ if (isAuthURL) {
+ // We can assume that we have no pending fetchers, since we only
+ // handle failure to load the initial authorization URL
+ [self closeTheWindow];
+ [self invokeFinalCallbackWithError:error];
+ return YES;
+ }
+ return NO;
+}
+
+- (void)authCodeObtained {
+ // the callback page was requested, or the authenticate code was loaded
+ // into a page's title, so exchange the auth code for access & refresh tokens
+ // and tell the window to close
+
+ // avoid duplicate signals that the callback point has been reached
+ self.hasHandledCallback = YES;
+
+ // If the signin was request for exchanging an authentication token to a
+ // refresh token, there is no window to close.
+ if (self.webRequestSelector) {
+ [self closeTheWindow];
+ } else {
+ // For signing in to Google, append the scope for obtaining the
+ // authenticated user email and profile, as appropriate. This is usually
+ // done by the startSigningIn method, but this method is not called when
+ // exchanging an authentication token for a refresh token.
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ [self addScopeForGoogleUserInfo];
+#endif
+ }
+
+ NSError *error = nil;
+
+ GTMOAuth2Authentication *auth = self.authentication;
+ NSString *code = auth.code;
+ if ([code length] > 0) {
+ // exchange the code for a token
+ SEL sel = @selector(auth:finishedWithFetcher:error:);
+ GTMHTTPFetcher *fetcher = [auth beginTokenFetchWithDelegate:self
+ didFinishSelector:sel];
+ if (fetcher == nil) {
+ error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:-1
+ userInfo:nil];
+ } else {
+ self.pendingFetcher = fetcher;
+ }
+
+ // notify the app so it can put up a post-sign in, pre-token exchange UI
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMOAuth2UserSignedIn
+ object:self
+ userInfo:nil];
+ } else {
+ // the callback lacked an auth code
+ NSString *errStr = auth.errorString;
+ NSDictionary *userInfo = nil;
+ if ([errStr length] > 0) {
+ userInfo = [NSDictionary dictionaryWithObject:errStr
+ forKey:kGTMOAuth2ErrorMessageKey];
+ }
+
+ error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
+ code:kGTMOAuth2ErrorAuthorizationFailed
+ userInfo:userInfo];
+ }
+
+ if (error) {
+ [self finishSignInWithError:error];
+ }
+}
+
+- (void)auth:(GTMOAuth2Authentication *)auth
+finishedWithFetcher:(GTMHTTPFetcher *)fetcher
+ error:(NSError *)error {
+ self.pendingFetcher = nil;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ if (error == nil
+ && (self.shouldFetchGoogleUserEmail || self.shouldFetchGoogleUserProfile)
+ && [self.authentication.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
+ // fetch the user's information from the Google server
+ [self fetchGoogleUserInfo];
+ } else {
+ // we're not authorizing with Google, so we're done
+ [self finishSignInWithError:error];
+ }
+#else
+ [self finishSignInWithError:error];
+#endif
+}
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth {
+ // create a fetcher for obtaining the user's email or profile
+ NSURL *infoURL = [[self class] googleUserInfoURL];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:infoURL];
+
+ if ([auth respondsToSelector:@selector(userAgent)]) {
+ NSString *userAgent = [auth userAgent];
+ [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+ }
+ [request setValue:@"no-cache" forHTTPHeaderField:@"Cache-Control"];
+
+ GTMHTTPFetcher *fetcher;
+ id <GTMHTTPFetcherServiceProtocol> fetcherService = nil;
+ if ([auth respondsToSelector:@selector(fetcherService)]) {
+ fetcherService = auth.fetcherService;
+ };
+ if (fetcherService) {
+ fetcher = [fetcherService fetcherWithRequest:request];
+ } else {
+ fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
+ }
+ fetcher.authorizer = auth;
+ fetcher.retryEnabled = YES;
+ fetcher.maxRetryInterval = 15.0;
+ fetcher.comment = @"user info";
+ return fetcher;
+}
+
+- (void)fetchGoogleUserInfo {
+ if (!self.shouldFetchGoogleUserProfile) {
+ // If we only need email and user ID, not the full profile, and we have an
+ // id_token, it may have the email and user ID so we won't need to fetch
+ // them.
+ GTMOAuth2Authentication *auth = self.authentication;
+ NSString *idToken = [auth.parameters objectForKey:@"id_token"];
+ if ([idToken length] > 0) {
+ // The id_token has three dot-delimited parts. The second is the
+ // JSON profile.
+ //
+ // http://www.tbray.org/ongoing/When/201x/2013/04/04/ID-Tokens
+ NSArray *parts = [idToken componentsSeparatedByString:@"."];
+ if ([parts count] == 3) {
+ NSString *part2 = [parts objectAtIndex:1];
+ if ([part2 length] > 0) {
+ NSData *data = [[self class] decodeWebSafeBase64:part2];
+ if ([data length] > 0) {
+ [self updateGoogleUserInfoWithData:data];
+ if ([[auth userID] length] > 0 && [[auth userEmail] length] > 0) {
+ // We obtained user ID and email from the ID token.
+ [self finishSignInWithError:nil];
+ return;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ // Fetch the email and profile from the userinfo endpoint.
+ GTMOAuth2Authentication *auth = self.authentication;
+ GTMHTTPFetcher *fetcher = [[self class] userInfoFetcherWithAuth:auth];
+ [fetcher beginFetchWithDelegate:self
+ didFinishSelector:@selector(infoFetcher:finishedWithData:error:)];
+
+ self.pendingFetcher = fetcher;
+
+ [auth notifyFetchIsRunning:YES
+ fetcher:fetcher
+ type:kGTMOAuth2FetchTypeUserInfo];
+}
+
+- (void)infoFetcher:(GTMHTTPFetcher *)fetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error {
+ GTMOAuth2Authentication *auth = self.authentication;
+ [auth notifyFetchIsRunning:NO
+ fetcher:fetcher
+ type:nil];
+
+ self.pendingFetcher = nil;
+
+ if (error) {
+#if DEBUG
+ if (data) {
+ NSString *dataStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ NSLog(@"infoFetcher error: %@\n%@", error, dataStr);
+ }
+#endif
+ } else {
+ // We have the authenticated user's info
+ [self updateGoogleUserInfoWithData:data];
+ }
+ [self finishSignInWithError:error];
+}
+
+- (void)updateGoogleUserInfoWithData:(NSData *)data {
+ if (!data) return;
+
+ GTMOAuth2Authentication *auth = self.authentication;
+ NSDictionary *profileDict = [[auth class] dictionaryWithJSONData:data];
+ if (profileDict) {
+ self.userProfile = profileDict;
+
+ // Save the ID into the auth object
+ NSString *identifier = [profileDict objectForKey:@"id"];
+ [auth setUserID:identifier];
+
+ // Save the email into the auth object
+ NSString *email = [profileDict objectForKey:@"email"];
+ [auth setUserEmail:email];
+
+ // The verified_email key is a boolean NSNumber in the userinfo
+ // endpoint response, but it is a string like "true" in the id_token.
+ // We want to consistently save it as a string of the boolean value,
+ // like @"1".
+ id verified = [profileDict objectForKey:@"verified_email"];
+ if ([verified isKindOfClass:[NSString class]]) {
+ verified = [NSNumber numberWithBool:[verified boolValue]];
+ }
+
+ [auth setUserEmailIsVerified:[verified stringValue]];
+ }
+}
+
+#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+
+- (void)finishSignInWithError:(NSError *)error {
+ [self invokeFinalCallbackWithError:error];
+}
+
+// convenience method for making the final call to our delegate
+- (void)invokeFinalCallbackWithError:(NSError *)error {
+ if (delegate_ && finishedSelector_) {
+ GTMOAuth2Authentication *auth = self.authentication;
+
+ NSMethodSignature *sig = [delegate_ methodSignatureForSelector:finishedSelector_];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:finishedSelector_];
+ [invocation setTarget:delegate_];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&auth atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+
+ // we'll no longer send messages to the delegate
+ //
+ // we want to autorelease it rather than assign to the property in case
+ // the delegate is below us in the call stack
+ [delegate_ autorelease];
+ delegate_ = nil;
+}
+
+#pragma mark Reachability monitoring
+
+static void ReachabilityCallBack(SCNetworkReachabilityRef target,
+ SCNetworkConnectionFlags flags,
+ void *info) {
+ // pass the flags to the signIn object
+ GTMOAuth2SignIn *signIn = (GTMOAuth2SignIn *)info;
+
+ [signIn reachabilityTarget:target
+ changedFlags:flags];
+}
+
+- (void)startReachabilityCheck {
+ // the user may set the timeout to 0 to skip the reachability checking
+ // during display of the sign-in page
+ if (networkLossTimeoutInterval_ <= 0.0 || reachabilityRef_ != NULL) {
+ return;
+ }
+
+ // create a reachability target from the authorization URL, add our callback,
+ // and schedule it on the run loop so we'll be notified if the network drops
+ NSURL *url = self.authorizationURL;
+ const char* host = [[url host] UTF8String];
+ reachabilityRef_ = SCNetworkReachabilityCreateWithName(kCFAllocatorSystemDefault,
+ host);
+ if (reachabilityRef_) {
+ BOOL isScheduled = NO;
+ SCNetworkReachabilityContext ctx = { 0, self, NULL, NULL, NULL };
+
+ if (SCNetworkReachabilitySetCallback(reachabilityRef_,
+ ReachabilityCallBack, &ctx)) {
+ if (SCNetworkReachabilityScheduleWithRunLoop(reachabilityRef_,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode)) {
+ isScheduled = YES;
+ }
+ }
+
+ if (!isScheduled) {
+ CFRelease(reachabilityRef_);
+ reachabilityRef_ = NULL;
+ }
+ }
+}
+
+- (void)destroyUnreachabilityTimer {
+ [networkLossTimer_ invalidate];
+ [networkLossTimer_ autorelease];
+ networkLossTimer_ = nil;
+}
+
+- (void)reachabilityTarget:(SCNetworkReachabilityRef)reachabilityRef
+ changedFlags:(SCNetworkConnectionFlags)flags {
+ BOOL isConnected = (flags & kSCNetworkFlagsReachable) != 0
+ && (flags & kSCNetworkFlagsConnectionRequired) == 0;
+
+ if (isConnected) {
+ // server is again reachable
+ [self destroyUnreachabilityTimer];
+
+ if (hasNotifiedNetworkLoss_) {
+ // tell the user that the network has been found
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMOAuth2NetworkFound
+ object:self
+ userInfo:nil];
+ hasNotifiedNetworkLoss_ = NO;
+ }
+ } else {
+ // the server has become unreachable; start the timer, if necessary
+ if (networkLossTimer_ == nil
+ && networkLossTimeoutInterval_ > 0
+ && !hasNotifiedNetworkLoss_) {
+ SEL sel = @selector(reachabilityTimerFired:);
+ networkLossTimer_ = [[NSTimer scheduledTimerWithTimeInterval:networkLossTimeoutInterval_
+ target:self
+ selector:sel
+ userInfo:nil
+ repeats:NO] retain];
+ }
+ }
+}
+
+- (void)reachabilityTimerFired:(NSTimer *)timer {
+ // the user may call [[notification object] cancelSigningIn] to
+ // dismiss the sign-in
+ if (!hasNotifiedNetworkLoss_) {
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMOAuth2NetworkLost
+ object:self
+ userInfo:nil];
+ hasNotifiedNetworkLoss_ = YES;
+ }
+
+ [self destroyUnreachabilityTimer];
+}
+
+- (void)stopReachabilityCheck {
+ [self destroyUnreachabilityTimer];
+
+ if (reachabilityRef_) {
+ SCNetworkReachabilityUnscheduleFromRunLoop(reachabilityRef_,
+ CFRunLoopGetCurrent(),
+ kCFRunLoopDefaultMode);
+ SCNetworkReachabilitySetCallback(reachabilityRef_, NULL, NULL);
+
+ CFRelease(reachabilityRef_);
+ reachabilityRef_ = NULL;
+ }
+}
+
+#pragma mark Token Revocation
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
+ if (auth.refreshToken != nil
+ && auth.canAuthorize
+ && [auth.serviceProvider isEqual:kGTMOAuth2ServiceProviderGoogle]) {
+
+ // create a signed revocation request for this authentication object
+ NSURL *url = [self googleRevocationURL];
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
+ [request setValue:@"application/x-www-form-urlencoded" forHTTPHeaderField:@"Content-Type"];
+
+ NSString *token = auth.refreshToken;
+ NSString *encoded = [GTMOAuth2Authentication encodedOAuthValueForString:token];
+ if (encoded != nil) {
+ NSString *body = [@"token=" stringByAppendingString:encoded];
+
+ [request setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];
+ [request setHTTPMethod:@"POST"];
+
+ NSString *userAgent = [auth userAgent];
+ [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+
+ // there's nothing to be done if revocation succeeds or fails
+ GTMHTTPFetcher *fetcher;
+ id <GTMHTTPFetcherServiceProtocol> fetcherService = auth.fetcherService;
+ if (fetcherService) {
+ fetcher = [fetcherService fetcherWithRequest:request];
+ } else {
+ fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
+ }
+ fetcher.comment = @"revoke token";
+
+ // Use a completion handler fetch for better debugging, but only if we're
+ // guaranteed that blocks are available in the runtime
+#if (!TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MIN_REQUIRED >= 1060)) || \
+ (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MIN_REQUIRED >= 40000))
+ // Blocks are available
+ [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) {
+ #if DEBUG
+ if (error) {
+ NSString *errStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ NSLog(@"revoke error: %@", errStr);
+ }
+ #endif // DEBUG
+ }];
+#else
+ // Blocks may not be available
+ [fetcher beginFetchWithDelegate:nil didFinishSelector:NULL];
+#endif
+ }
+ }
+ [auth reset];
+}
+
+
+// Based on Cyrus Najmabadi's elegent little encoder and decoder from
+// http://www.cocoadev.com/index.pl?BaseSixtyFour and on GTLBase64
+
++ (NSData *)decodeWebSafeBase64:(NSString *)base64Str {
+ static char decodingTable[128];
+ static BOOL hasInited = NO;
+
+ if (!hasInited) {
+ char webSafeEncodingTable[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
+ memset(decodingTable, 0, 128);
+ for (unsigned int i = 0; i < sizeof(webSafeEncodingTable); i++) {
+ decodingTable[(unsigned int) webSafeEncodingTable[i]] = (char)i;
+ }
+ hasInited = YES;
+ }
+
+ // The input string should be plain ASCII.
+ const char *cString = [base64Str cStringUsingEncoding:NSASCIIStringEncoding];
+ if (cString == nil) return nil;
+
+ NSInteger inputLength = (NSInteger)strlen(cString);
+ // Input length is not being restricted to multiples of 4.
+ if (inputLength == 0) return [NSData data];
+
+ while (inputLength > 0 && cString[inputLength - 1] == '=') {
+ inputLength--;
+ }
+
+ NSInteger outputLength = inputLength * 3 / 4;
+ NSMutableData* data = [NSMutableData dataWithLength:(NSUInteger)outputLength];
+ uint8_t *output = [data mutableBytes];
+
+ NSInteger inputPoint = 0;
+ NSInteger outputPoint = 0;
+ char *table = decodingTable;
+
+ while (inputPoint < inputLength - 1) {
+ int i0 = cString[inputPoint++];
+ int i1 = cString[inputPoint++];
+ int i2 = inputPoint < inputLength ? cString[inputPoint++] : 'A'; // 'A' will decode to \0
+ int i3 = inputPoint < inputLength ? cString[inputPoint++] : 'A';
+
+ output[outputPoint++] = (uint8_t)((table[i0] << 2) | (table[i1] >> 4));
+ if (outputPoint < outputLength) {
+ output[outputPoint++] = (uint8_t)(((table[i1] & 0xF) << 4) | (table[i2] >> 2));
+ }
+ if (outputPoint < outputLength) {
+ output[outputPoint++] = (uint8_t)(((table[i2] & 0x3) << 6) | table[i3]);
+ }
+ }
+
+ return data;
+}
+
+#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+
+@end
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES