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