aboutsummaryrefslogtreecommitdiffhomepage
path: root/example/common/gtm-oauth2/Source
diff options
context:
space:
mode:
Diffstat (limited to 'example/common/gtm-oauth2/Source')
-rw-r--r--example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h356
-rw-r--r--example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m1275
-rw-r--r--example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h187
-rw-r--r--example/common/gtm-oauth2/Source/GTMOAuth2SignIn.m939
-rw-r--r--example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib109
-rw-r--r--example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h332
-rw-r--r--example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m727
-rw-r--r--example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h376
-rw-r--r--example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m1070
-rw-r--r--example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib494
10 files changed, 5865 insertions, 0 deletions
diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h
new file mode 100644
index 00000000..8703164b
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.h
@@ -0,0 +1,356 @@
+/* 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
+
+// This class implements the OAuth 2 protocol for authorizing requests.
+// http://tools.ietf.org/html/draft-ietf-oauth-v2
+
+#import <Foundation/Foundation.h>
+
+// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
+#import "GTMHTTPFetcher.h"
+
+#undef _EXTERN
+#undef _INITIALIZE_AS
+#ifdef GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS
+ #define _EXTERN
+ #define _INITIALIZE_AS(x) =x
+#else
+ #if defined(__cplusplus)
+ #define _EXTERN extern "C"
+ #else
+ #define _EXTERN extern
+ #endif
+ #define _INITIALIZE_AS(x)
+#endif
+
+// Until all OAuth 2 providers are up to the same spec, we'll provide a crude
+// way here to override the "Bearer" string in the Authorization header
+#ifndef GTM_OAUTH2_BEARER
+#define GTM_OAUTH2_BEARER "Bearer"
+#endif
+
+// Service provider name allows stored authorization to be associated with
+// the authorizing service
+_EXTERN NSString* const kGTMOAuth2ServiceProviderGoogle _INITIALIZE_AS(@"Google");
+
+//
+// GTMOAuth2SignIn constants, included here for use by clients
+//
+_EXTERN NSString* const kGTMOAuth2ErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuth2");
+
+// Error userInfo keys
+_EXTERN NSString* const kGTMOAuth2ErrorMessageKey _INITIALIZE_AS(@"error");
+_EXTERN NSString* const kGTMOAuth2ErrorRequestKey _INITIALIZE_AS(@"request");
+_EXTERN NSString* const kGTMOAuth2ErrorJSONKey _INITIALIZE_AS(@"json");
+
+enum {
+ // Error code indicating that the window was prematurely closed
+ kGTMOAuth2ErrorWindowClosed = -1000,
+ kGTMOAuth2ErrorAuthorizationFailed = -1001,
+ kGTMOAuth2ErrorTokenExpired = -1002,
+ kGTMOAuth2ErrorTokenUnavailable = -1003,
+ kGTMOAuth2ErrorUnauthorizableRequest = -1004
+};
+
+
+// Notifications for token fetches
+_EXTERN NSString* const kGTMOAuth2FetchStarted _INITIALIZE_AS(@"kGTMOAuth2FetchStarted");
+_EXTERN NSString* const kGTMOAuth2FetchStopped _INITIALIZE_AS(@"kGTMOAuth2FetchStopped");
+
+_EXTERN NSString* const kGTMOAuth2FetcherKey _INITIALIZE_AS(@"fetcher");
+_EXTERN NSString* const kGTMOAuth2FetchTypeKey _INITIALIZE_AS(@"FetchType");
+_EXTERN NSString* const kGTMOAuth2FetchTypeToken _INITIALIZE_AS(@"token");
+_EXTERN NSString* const kGTMOAuth2FetchTypeRefresh _INITIALIZE_AS(@"refresh");
+_EXTERN NSString* const kGTMOAuth2FetchTypeAssertion _INITIALIZE_AS(@"assertion");
+_EXTERN NSString* const kGTMOAuth2FetchTypeUserInfo _INITIALIZE_AS(@"userInfo");
+
+// Token-issuance errors
+_EXTERN NSString* const kGTMOAuth2ErrorKey _INITIALIZE_AS(@"error");
+_EXTERN NSString* const kGTMOAuth2ErrorObjectKey _INITIALIZE_AS(@"kGTMOAuth2ErrorObjectKey");
+
+_EXTERN NSString* const kGTMOAuth2ErrorInvalidRequest _INITIALIZE_AS(@"invalid_request");
+_EXTERN NSString* const kGTMOAuth2ErrorInvalidClient _INITIALIZE_AS(@"invalid_client");
+_EXTERN NSString* const kGTMOAuth2ErrorInvalidGrant _INITIALIZE_AS(@"invalid_grant");
+_EXTERN NSString* const kGTMOAuth2ErrorUnauthorizedClient _INITIALIZE_AS(@"unauthorized_client");
+_EXTERN NSString* const kGTMOAuth2ErrorUnsupportedGrantType _INITIALIZE_AS(@"unsupported_grant_type");
+_EXTERN NSString* const kGTMOAuth2ErrorInvalidScope _INITIALIZE_AS(@"invalid_scope");
+
+// Notification that sign-in has completed, and token fetches will begin (useful
+// for displaying interstitial messages after the window has closed)
+_EXTERN NSString* const kGTMOAuth2UserSignedIn _INITIALIZE_AS(@"kGTMOAuth2UserSignedIn");
+
+// Notification for token changes
+_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshed");
+_EXTERN NSString* const kGTMOAuth2RefreshTokenChanged _INITIALIZE_AS(@"kGTMOAuth2RefreshTokenChanged");
+_EXTERN NSString* const kGTMOAuth2AccessTokenRefreshFailed _INITIALIZE_AS(@"kGTMOAuth2AccessTokenRefreshFailed");
+
+// Notification for WebView loading
+_EXTERN NSString* const kGTMOAuth2WebViewStartedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStartedLoading");
+_EXTERN NSString* const kGTMOAuth2WebViewStoppedLoading _INITIALIZE_AS(@"kGTMOAuth2WebViewStoppedLoading");
+_EXTERN NSString* const kGTMOAuth2WebViewKey _INITIALIZE_AS(@"kGTMOAuth2WebViewKey");
+_EXTERN NSString* const kGTMOAuth2WebViewStopKindKey _INITIALIZE_AS(@"kGTMOAuth2WebViewStopKindKey");
+_EXTERN NSString* const kGTMOAuth2WebViewFinished _INITIALIZE_AS(@"finished");
+_EXTERN NSString* const kGTMOAuth2WebViewFailed _INITIALIZE_AS(@"failed");
+_EXTERN NSString* const kGTMOAuth2WebViewCancelled _INITIALIZE_AS(@"cancelled");
+
+// Notification for network loss during html sign-in display
+_EXTERN NSString* const kGTMOAuth2NetworkLost _INITIALIZE_AS(@"kGTMOAuthNetworkLost");
+_EXTERN NSString* const kGTMOAuth2NetworkFound _INITIALIZE_AS(@"kGTMOAuthNetworkFound");
+
+@interface GTMOAuth2Authentication : NSObject <GTMFetcherAuthorizationProtocol> {
+ @private
+ NSString *clientID_;
+ NSString *clientSecret_;
+ NSString *redirectURI_;
+ NSMutableDictionary *parameters_;
+
+ // authorization parameters
+ NSURL *tokenURL_;
+ NSDate *expirationDate_;
+
+ NSString *authorizationTokenKey_;
+
+ NSDictionary *additionalTokenRequestParameters_;
+ NSDictionary *additionalGrantTypeRequestParameters_;
+
+ // queue of requests for authorization waiting for a valid access token
+ GTMHTTPFetcher *refreshFetcher_;
+ NSMutableArray *authorizationQueue_;
+
+ id <GTMHTTPFetcherServiceProtocol> fetcherService_; // WEAK
+
+ Class parserClass_;
+
+ BOOL shouldAuthorizeAllRequests_;
+
+ // arbitrary data retained for the user
+ id userData_;
+ NSMutableDictionary *properties_;
+}
+
+// OAuth2 standard protocol parameters
+//
+// These should be the plain strings; any needed escaping will be provided by
+// the library.
+
+// Request properties
+@property (copy) NSString *clientID;
+@property (copy) NSString *clientSecret;
+@property (copy) NSString *redirectURI;
+@property (retain) NSString *scope;
+@property (retain) NSString *tokenType;
+@property (retain) NSString *assertion;
+@property (retain) NSString *refreshScope;
+
+// Apps may optionally add parameters here to be provided to the token
+// endpoint on token requests and refreshes.
+@property (retain) NSDictionary *additionalTokenRequestParameters;
+
+// Apps may optionally add parameters here to be provided to the token
+// endpoint on specific token requests and refreshes, keyed by the grant_type.
+// For example, if a different "type" parameter is required for obtaining
+// the auth code and on refresh, this might be:
+//
+// viewController.authentication.additionalGrantTypeRequestParameters = @{
+// @"authorization_code" : @{ @"type" : @"code" },
+// @"refresh_token" : @{ @"type" : @"refresh" }
+// };
+@property (retain) NSDictionary *additionalGrantTypeRequestParameters;
+
+// Response properties
+@property (retain) NSMutableDictionary *parameters;
+
+@property (retain) NSString *accessToken;
+@property (retain) NSString *refreshToken;
+@property (retain) NSNumber *expiresIn;
+@property (retain) NSString *code;
+@property (retain) NSString *errorString;
+
+// URL for obtaining access tokens
+@property (copy) NSURL *tokenURL;
+
+// Calculated expiration date (expiresIn seconds added to the
+// time the access token was received.)
+@property (copy) NSDate *expirationDate;
+
+// Service identifier, like "Google"; not used for authentication
+//
+// The provider name is just for allowing stored authorization to be associated
+// with the authorizing service.
+@property (copy) NSString *serviceProvider;
+
+// User ID; not used for authentication
+@property (retain) NSString *userID;
+
+// User email and verified status; not used for authentication
+//
+// The verified string can be checked with -boolValue. If the result is false,
+// then the email address is listed with the account on the server, but the
+// address has not been confirmed as belonging to the owner of the account.
+@property (retain) NSString *userEmail;
+@property (retain) NSString *userEmailIsVerified;
+
+// Property indicating if this auth has a refresh or access token so is suitable
+// for authorizing a request. This does not guarantee that the token is valid.
+@property (readonly) BOOL canAuthorize;
+
+// Property indicating if this object will authorize plain http request
+// (as well as any non-https requests.) Default is NO, only requests with the
+// scheme https are authorized, since security may be compromised if tokens
+// are sent over the wire using an unencrypted protocol like http.
+@property (assign) BOOL shouldAuthorizeAllRequests;
+
+// userData is retained for the convenience of the caller
+@property (retain) id userData;
+
+// Stored property values are retained for the convenience of the caller
+@property (retain) NSDictionary *properties;
+
+// Property for the optional fetcher service instance to be used to create
+// fetchers
+//
+// Fetcher service objects retain authorizations, so this is weak to avoid
+// circular retains.
+@property (assign) id <GTMHTTPFetcherServiceProtocol> fetcherService; // WEAK
+
+// Alternative JSON parsing class; this should implement the
+// GTMOAuth2ParserClass informal protocol. If this property is
+// not set, the class SBJSON must be available in the runtime.
+@property (assign) Class parserClass;
+
+// Key for the response parameter used for the authorization header; by default,
+// "access_token" is used, but some servers may expect alternatives, like
+// "id_token".
+@property (copy) NSString *authorizationTokenKey;
+
+// Convenience method for creating an authentication object
++ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
+ tokenURL:(NSURL *)tokenURL
+ redirectURI:(NSString *)redirectURI
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret;
+
+// Clear out any authentication values, prepare for a new request fetch
+- (void)reset;
+
+// Main authorization entry points
+//
+// These will refresh the access token, if necessary, add the access token to
+// the request, then invoke the callback.
+//
+// The request argument may be nil to just force a refresh of the access token,
+// if needed.
+//
+// NOTE: To avoid accidental leaks of bearer tokens, the request must
+// be for a URL with the scheme https unless the shouldAuthorizeAllRequests
+// property is set.
+
+// The finish selector should have a signature matching
+// - (void)authentication:(GTMOAuth2Authentication *)auth
+// request:(NSMutableURLRequest *)request
+// finishedWithError:(NSError *)error;
+
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel;
+
+#if NS_BLOCKS_AVAILABLE
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ completionHandler:(void (^)(NSError *error))handler;
+#endif
+
+// Synchronous entry point; authorizing this way cannot refresh an expired
+// access token
+- (BOOL)authorizeRequest:(NSMutableURLRequest *)request;
+
+// If the authentication is waiting for a refresh to complete, spin the run
+// loop, discarding events, until the fetch has completed
+//
+// This is only for use in testing or in tools without a user interface.
+- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds;
+
+
+//////////////////////////////////////////////////////////////////////////////
+//
+// Internal properties and methods for use by GTMOAuth2SignIn
+//
+
+// Pending fetcher to get a new access token, if any
+@property (retain) GTMHTTPFetcher *refreshFetcher;
+
+// Check if a request is queued up to be authorized
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request;
+
+// Check if a request appears to be authorized
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request;
+
+// Stop any pending refresh fetch. This will also cancel the authorization
+// for all fetch requests pending authorization.
+- (void)stopAuthorization;
+
+// Prevents authorization callback for a given request.
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request;
+
+// OAuth fetch user-agent header value
+- (NSString *)userAgent;
+
+// Parse and set token and token secret from response data
+- (void)setKeysForResponseString:(NSString *)str;
+- (void)setKeysForResponseDictionary:(NSDictionary *)dict;
+
+// Persistent token string for keychain storage
+//
+// We'll use the format "refresh_token=foo&serviceProvider=bar" so we can
+// easily alter what portions of the auth data are stored
+//
+// Use these methods for serialization
+- (NSString *)persistenceResponseString;
+- (void)setKeysForPersistenceResponseString:(NSString *)str;
+
+// method to begin fetching an access token, used by the sign-in object
+- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
+ didFinishSelector:(SEL)finishedSel;
+
+// Entry point to post a notification about a fetcher currently used for
+// obtaining or refreshing a token; the sign-in object will also use this
+// to indicate when the user's email address is being fetched.
+//
+// Fetch type constants are above under "notifications for token fetches"
+- (void)notifyFetchIsRunning:(BOOL)isStarting
+ fetcher:(GTMHTTPFetcher *)fetcher
+ type:(NSString *)fetchType;
+
+// Arbitrary key-value properties retained for the user
+- (void)setProperty:(id)obj forKey:(NSString *)key;
+- (id)propertyForKey:(NSString *)key;
+
+//
+// Utilities
+//
+
++ (NSString *)encodedOAuthValueForString:(NSString *)str;
+
++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
+
++ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr;
+
++ (NSDictionary *)dictionaryWithJSONData:(NSData *)data;
+
++ (NSString *)scopeWithStrings:(NSString *)firsStr, ... NS_REQUIRES_NIL_TERMINATION;
+@end
+
+#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m
new file mode 100644
index 00000000..b0c99776
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m
@@ -0,0 +1,1275 @@
+/* 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 GTMOAUTH2AUTHENTICATION_DEFINE_GLOBALS 1
+#import "GTMOAuth2Authentication.h"
+
+// standard OAuth keys
+static NSString *const kOAuth2AccessTokenKey = @"access_token";
+static NSString *const kOAuth2RefreshTokenKey = @"refresh_token";
+static NSString *const kOAuth2ClientIDKey = @"client_id";
+static NSString *const kOAuth2ClientSecretKey = @"client_secret";
+static NSString *const kOAuth2RedirectURIKey = @"redirect_uri";
+static NSString *const kOAuth2ResponseTypeKey = @"response_type";
+static NSString *const kOAuth2ScopeKey = @"scope";
+static NSString *const kOAuth2ErrorKey = @"error";
+static NSString *const kOAuth2TokenTypeKey = @"token_type";
+static NSString *const kOAuth2ExpiresInKey = @"expires_in";
+static NSString *const kOAuth2CodeKey = @"code";
+static NSString *const kOAuth2AssertionKey = @"assertion";
+static NSString *const kOAuth2RefreshScopeKey = @"refreshScope";
+
+// additional persistent keys
+static NSString *const kServiceProviderKey = @"serviceProvider";
+static NSString *const kUserIDKey = @"userID";
+static NSString *const kUserEmailKey = @"email";
+static NSString *const kUserEmailIsVerifiedKey = @"isVerified";
+
+// fetcher keys
+static NSString *const kTokenFetchDelegateKey = @"delegate";
+static NSString *const kTokenFetchSelectorKey = @"sel";
+
+static NSString *const kRefreshFetchArgsKey = @"requestArgs";
+
+// If GTMNSJSONSerialization is available, it is used for formatting JSON
+#if (TARGET_OS_MAC && !TARGET_OS_IPHONE && (MAC_OS_X_VERSION_MAX_ALLOWED < 1070)) || \
+ (TARGET_OS_IPHONE && (__IPHONE_OS_VERSION_MAX_ALLOWED < 50000))
+@interface GTMNSJSONSerialization : NSObject
++ (id)JSONObjectWithData:(NSData *)data options:(NSUInteger)opt error:(NSError **)error;
+@end
+#endif
+
+@interface GTMOAuth2ParserClass : NSObject
+// just enough of SBJSON to be able to parse
+- (id)objectWithString:(NSString*)repr error:(NSError**)error;
+@end
+
+// wrapper class for requests needing authorization and their callbacks
+@interface GTMOAuth2AuthorizationArgs : NSObject {
+ @private
+ NSMutableURLRequest *request_;
+ id delegate_;
+ SEL sel_;
+ id completionHandler_;
+ NSThread *thread_;
+ NSError *error_;
+}
+
+@property (retain) NSMutableURLRequest *request;
+@property (retain) id delegate;
+@property (assign) SEL selector;
+@property (copy) id completionHandler;
+@property (retain) NSThread *thread;
+@property (retain) NSError *error;
+
++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
+ delegate:(id)delegate
+ selector:(SEL)sel
+ completionHandler:(id)completionHandler
+ thread:(NSThread *)thread;
+@end
+
+@implementation GTMOAuth2AuthorizationArgs
+
+@synthesize request = request_,
+ delegate = delegate_,
+ selector = sel_,
+ completionHandler = completionHandler_,
+ thread = thread_,
+ error = error_;
+
++ (GTMOAuth2AuthorizationArgs *)argsWithRequest:(NSMutableURLRequest *)req
+ delegate:(id)delegate
+ selector:(SEL)sel
+ completionHandler:(id)completionHandler
+ thread:(NSThread *)thread {
+ GTMOAuth2AuthorizationArgs *obj;
+ obj = [[[GTMOAuth2AuthorizationArgs alloc] init] autorelease];
+ obj.request = req;
+ obj.delegate = delegate;
+ obj.selector = sel;
+ obj.completionHandler = completionHandler;
+ obj.thread = thread;
+ return obj;
+}
+
+- (void)dealloc {
+ [request_ release];
+ [delegate_ release];
+ [completionHandler_ release];
+ [thread_ release];
+ [error_ release];
+
+ [super dealloc];
+}
+@end
+
+
+@interface GTMOAuth2Authentication ()
+
+@property (retain) NSMutableArray *authorizationQueue;
+@property (readonly) NSString *authorizationToken;
+
+- (void)setKeysForResponseJSONData:(NSData *)data;
+
+- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args;
+
+- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args;
+
+- (BOOL)shouldRefreshAccessToken;
+
+- (void)updateExpirationDate;
+
+- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error;
+
+- (void)auth:(GTMOAuth2Authentication *)auth
+finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher
+ error:(NSError *)error;
+
+- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args;
+
++ (void)invokeDelegate:(id)delegate
+ selector:(SEL)sel
+ object:(id)obj1
+ object:(id)obj2
+ object:(id)obj3;
+
++ (NSString *)unencodedOAuthParameterForString:(NSString *)str;
++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict;
+
++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data;
+
+@end
+
+@implementation GTMOAuth2Authentication
+
+@synthesize clientID = clientID_,
+ clientSecret = clientSecret_,
+ redirectURI = redirectURI_,
+ parameters = parameters_,
+ authorizationTokenKey = authorizationTokenKey_,
+ tokenURL = tokenURL_,
+ expirationDate = expirationDate_,
+ additionalTokenRequestParameters = additionalTokenRequestParameters_,
+ additionalGrantTypeRequestParameters = additionalGrantTypeRequestParameters_,
+ refreshFetcher = refreshFetcher_,
+ fetcherService = fetcherService_,
+ parserClass = parserClass_,
+ shouldAuthorizeAllRequests = shouldAuthorizeAllRequests_,
+ userData = userData_,
+ properties = properties_,
+ authorizationQueue = authorizationQueue_;
+
+// Response parameters
+@dynamic accessToken,
+ refreshToken,
+ code,
+ assertion,
+ refreshScope,
+ errorString,
+ tokenType,
+ scope,
+ expiresIn,
+ serviceProvider,
+ userEmail,
+ userEmailIsVerified;
+
+@dynamic canAuthorize;
+
++ (id)authenticationWithServiceProvider:(NSString *)serviceProvider
+ tokenURL:(NSURL *)tokenURL
+ redirectURI:(NSString *)redirectURI
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret {
+ GTMOAuth2Authentication *obj = [[[self alloc] init] autorelease];
+ obj.serviceProvider = serviceProvider;
+ obj.tokenURL = tokenURL;
+ obj.redirectURI = redirectURI;
+ obj.clientID = clientID;
+ obj.clientSecret = clientSecret;
+ return obj;
+}
+
+- (id)init {
+ self = [super init];
+ if (self) {
+ authorizationQueue_ = [[NSMutableArray alloc] init];
+ parameters_ = [[NSMutableDictionary alloc] init];
+ }
+ return self;
+}
+
+- (NSString *)description {
+ NSArray *props = [NSArray arrayWithObjects:@"accessToken", @"refreshToken",
+ @"code", @"assertion", @"expirationDate", @"errorString",
+ nil];
+ NSMutableString *valuesStr = [NSMutableString string];
+ NSString *separator = @"";
+ for (NSString *prop in props) {
+ id result = [self valueForKey:prop];
+ if (result) {
+ [valuesStr appendFormat:@"%@%@=\"%@\"", separator, prop, result];
+ separator = @", ";
+ }
+ }
+
+ return [NSString stringWithFormat:@"%@ %p: {%@}",
+ [self class], self, valuesStr];
+}
+
+- (void)dealloc {
+ [clientID_ release];
+ [clientSecret_ release];
+ [redirectURI_ release];
+ [parameters_ release];
+ [authorizationTokenKey_ release];
+ [tokenURL_ release];
+ [expirationDate_ release];
+ [additionalTokenRequestParameters_ release];
+ [additionalGrantTypeRequestParameters_ release];
+ [refreshFetcher_ release];
+ [authorizationQueue_ release];
+ [userData_ release];
+ [properties_ release];
+
+ [super dealloc];
+}
+
+#pragma mark -
+
+- (void)setKeysForResponseDictionary:(NSDictionary *)dict {
+ if (dict == nil) return;
+
+ // If a new code or access token is being set, remove the old expiration
+ NSString *newCode = [dict objectForKey:kOAuth2CodeKey];
+ NSString *newAccessToken = [dict objectForKey:kOAuth2AccessTokenKey];
+ if (newCode || newAccessToken) {
+ self.expiresIn = nil;
+ }
+
+ BOOL didRefreshTokenChange = NO;
+ NSString *refreshToken = [dict objectForKey:kOAuth2RefreshTokenKey];
+ if (refreshToken) {
+ NSString *priorRefreshToken = self.refreshToken;
+
+ if (priorRefreshToken != refreshToken
+ && (priorRefreshToken == nil
+ || ![priorRefreshToken isEqual:refreshToken])) {
+ didRefreshTokenChange = YES;
+ }
+ }
+
+ [self.parameters addEntriesFromDictionary:dict];
+ [self updateExpirationDate];
+
+ if (didRefreshTokenChange) {
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:kGTMOAuth2RefreshTokenChanged
+ object:self
+ userInfo:nil];
+ }
+ // NSLog(@"keys set ----------------------------\n%@", dict);
+}
+
+- (void)setKeysForResponseString:(NSString *)str {
+ NSDictionary *dict = [[self class] dictionaryWithResponseString:str];
+ [self setKeysForResponseDictionary:dict];
+}
+
+- (void)setKeysForResponseJSONData:(NSData *)data {
+ NSDictionary *dict = [[self class] dictionaryWithJSONData:data];
+ [self setKeysForResponseDictionary:dict];
+}
+
++ (NSDictionary *)dictionaryWithJSONData:(NSData *)data {
+ NSMutableDictionary *obj = nil;
+ NSError *error = nil;
+
+ Class serializer = NSClassFromString(@"NSJSONSerialization");
+ if (serializer) {
+ const NSUInteger kOpts = (1UL << 0); // NSJSONReadingMutableContainers
+ obj = [serializer JSONObjectWithData:data
+ options:kOpts
+ error:&error];
+#if DEBUG
+ if (error) {
+ NSString *str = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ NSLog(@"NSJSONSerialization error %@ parsing %@",
+ error, str);
+ }
+#endif
+ return obj;
+ } else {
+ // try SBJsonParser or SBJSON
+ Class jsonParseClass = NSClassFromString(@"SBJsonParser");
+ if (!jsonParseClass) {
+ jsonParseClass = NSClassFromString(@"SBJSON");
+ }
+ if (jsonParseClass) {
+ GTMOAuth2ParserClass *parser = [[[jsonParseClass alloc] init] autorelease];
+ NSString *jsonStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ if (jsonStr) {
+ obj = [parser objectWithString:jsonStr error:&error];
+#if DEBUG
+ if (error) {
+ NSLog(@"%@ error %@ parsing %@", NSStringFromClass(jsonParseClass),
+ error, jsonStr);
+ }
+#endif
+ return obj;
+ }
+ } else {
+#if DEBUG
+ NSAssert(0, @"GTMOAuth2Authentication: No parser available");
+#endif
+ }
+ }
+ return nil;
+}
+
+#pragma mark Authorizing Requests
+
+// General entry point for authorizing requests
+
+#if NS_BLOCKS_AVAILABLE
+// Authorizing with a completion block
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ completionHandler:(void (^)(NSError *error))handler {
+
+ GTMOAuth2AuthorizationArgs *args;
+ args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
+ delegate:nil
+ selector:NULL
+ completionHandler:handler
+ thread:[NSThread currentThread]];
+ [self authorizeRequestArgs:args];
+}
+#endif
+
+// Authorizing with a callback selector
+//
+// Selector has the signature
+// - (void)authentication:(GTMOAuth2Authentication *)auth
+// request:(NSMutableURLRequest *)request
+// finishedWithError:(NSError *)error;
+- (void)authorizeRequest:(NSMutableURLRequest *)request
+ delegate:(id)delegate
+ didFinishSelector:(SEL)sel {
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, sel,
+ @encode(GTMOAuth2Authentication *),
+ @encode(NSMutableURLRequest *),
+ @encode(NSError *), 0);
+
+ GTMOAuth2AuthorizationArgs *args;
+ args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
+ delegate:delegate
+ selector:sel
+ completionHandler:nil
+ thread:[NSThread currentThread]];
+ [self authorizeRequestArgs:args];
+}
+
+// Internal routine common to delegate and block invocations
+- (BOOL)authorizeRequestArgs:(GTMOAuth2AuthorizationArgs *)args {
+ BOOL didAttempt = NO;
+
+ @synchronized(authorizationQueue_) {
+
+ BOOL shouldRefresh = [self shouldRefreshAccessToken];
+
+ if (shouldRefresh) {
+ // attempt to refresh now; once we have a fresh access token, we will
+ // authorize the request and call back to the user
+ didAttempt = YES;
+
+ if (self.refreshFetcher == nil) {
+ // there's not already a refresh pending
+ SEL finishedSel = @selector(auth:finishedRefreshWithFetcher:error:);
+ self.refreshFetcher = [self beginTokenFetchWithDelegate:self
+ didFinishSelector:finishedSel];
+ if (self.refreshFetcher) {
+ [authorizationQueue_ addObject:args];
+ }
+ } else {
+ // there's already a refresh pending
+ [authorizationQueue_ addObject:args];
+ }
+ }
+
+ if (!shouldRefresh || self.refreshFetcher == nil) {
+ // we're not fetching a new access token, so we can authorize the request
+ // now
+ didAttempt = [self authorizeRequestImmediateArgs:args];
+ }
+ }
+ return didAttempt;
+}
+
+- (void)auth:(GTMOAuth2Authentication *)auth
+finishedRefreshWithFetcher:(GTMHTTPFetcher *)fetcher
+ error:(NSError *)error {
+ @synchronized(authorizationQueue_) {
+ // If there's an error, we want to try using the old access token anyway,
+ // in case it's a backend problem preventing refresh, in which case
+ // access tokens past their expiration date may still work
+
+ self.refreshFetcher = nil;
+
+ // Swap in a new auth queue in case the callbacks try to immediately auth
+ // another request
+ NSArray *pendingAuthQueue = [NSArray arrayWithArray:authorizationQueue_];
+ [authorizationQueue_ removeAllObjects];
+
+ BOOL hasAccessToken = ([self.accessToken length] > 0);
+
+ NSString *noteName;
+ NSDictionary *userInfo = nil;
+ if (hasAccessToken && error == nil) {
+ // Successful refresh.
+ noteName = kGTMOAuth2AccessTokenRefreshed;
+ userInfo = nil;
+ } else {
+ // Google's OAuth 2 implementation returns a 400 with JSON body
+ // containing error key "invalid_grant" to indicate the refresh token
+ // is invalid or has been revoked by the user. We'll promote the
+ // JSON error key's value for easy inspection by the observer.
+ noteName = kGTMOAuth2AccessTokenRefreshFailed;
+ NSString *jsonErr = nil;
+ if ([error code] == kGTMHTTPFetcherStatusBadRequest) {
+ NSDictionary *json = [[error userInfo] objectForKey:kGTMOAuth2ErrorJSONKey];
+ jsonErr = [json objectForKey:kGTMOAuth2ErrorMessageKey];
+ }
+ // error and jsonErr may be nil
+ userInfo = [NSMutableDictionary dictionary];
+ [userInfo setValue:error forKey:kGTMOAuth2ErrorObjectKey];
+ [userInfo setValue:jsonErr forKey:kGTMOAuth2ErrorMessageKey];
+ }
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:noteName
+ object:self
+ userInfo:userInfo];
+
+ for (GTMOAuth2AuthorizationArgs *args in pendingAuthQueue) {
+ if (!hasAccessToken && args.error == nil) {
+ args.error = error;
+ }
+
+ [self authorizeRequestImmediateArgs:args];
+ }
+ }
+}
+
+- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
+ BOOL wasFound = NO;
+ @synchronized(authorizationQueue_) {
+ for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) {
+ if ([args request] == request) {
+ wasFound = YES;
+ break;
+ }
+ }
+ }
+ return wasFound;
+}
+
+- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
+ NSString *authStr = [request valueForHTTPHeaderField:@"Authorization"];
+ return ([authStr length] > 0);
+}
+
+- (void)stopAuthorization {
+ @synchronized(authorizationQueue_) {
+ [authorizationQueue_ removeAllObjects];
+
+ [self.refreshFetcher stopFetching];
+ self.refreshFetcher = nil;
+ }
+}
+
+- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
+ @synchronized(authorizationQueue_) {
+ NSUInteger argIndex = 0;
+ BOOL found = NO;
+ for (GTMOAuth2AuthorizationArgs *args in authorizationQueue_) {
+ if ([args request] == request) {
+ found = YES;
+ break;
+ }
+ argIndex++;
+ }
+
+ if (found) {
+ [authorizationQueue_ removeObjectAtIndex:argIndex];
+
+ // If the queue is now empty, go ahead and stop the fetcher.
+ if ([authorizationQueue_ count] == 0) {
+ [self stopAuthorization];
+ }
+ }
+ }
+}
+
+- (BOOL)authorizeRequestImmediateArgs:(GTMOAuth2AuthorizationArgs *)args {
+ // This authorization entry point never attempts to refresh the access token,
+ // but does call the completion routine
+
+ NSMutableURLRequest *request = args.request;
+
+ NSString *scheme = [[request URL] scheme];
+ BOOL isAuthorizableRequest = self.shouldAuthorizeAllRequests
+ || [scheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
+ if (!isAuthorizableRequest) {
+ // Request is not https, so may be insecure
+ //
+ // The NSError will be created below
+#if DEBUG
+ NSLog(@"Cannot authorize request with scheme %@ (%@)", scheme, request);
+#endif
+ }
+
+ // Get the access token.
+ NSString *accessToken = self.authorizationToken;
+ if (isAuthorizableRequest && [accessToken length] > 0) {
+ if (request) {
+ // we have a likely valid access token
+ NSString *value = [NSString stringWithFormat:@"%s %@",
+ GTM_OAUTH2_BEARER, accessToken];
+ [request setValue:value forHTTPHeaderField:@"Authorization"];
+ }
+
+ // We've authorized the request, even if the previous refresh
+ // failed with an error
+ args.error = nil;
+ } else if (args.error == nil) {
+ NSDictionary *userInfo = nil;
+ if (request) {
+ userInfo = [NSDictionary dictionaryWithObject:request
+ forKey:kGTMOAuth2ErrorRequestKey];
+ }
+ NSInteger code = (isAuthorizableRequest ?
+ kGTMOAuth2ErrorAuthorizationFailed :
+ kGTMOAuth2ErrorUnauthorizableRequest);
+ args.error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
+ code:code
+ userInfo:userInfo];
+ }
+
+ // Invoke any callbacks on the proper thread
+ if (args.delegate || args.completionHandler) {
+ NSThread *targetThread = args.thread;
+ BOOL isSameThread = [targetThread isEqual:[NSThread currentThread]];
+
+ if (isSameThread) {
+ [self invokeCallbackArgs:args];
+ } else {
+ SEL sel = @selector(invokeCallbackArgs:);
+ NSOperationQueue *delegateQueue = self.fetcherService.delegateQueue;
+ if (delegateQueue) {
+ NSInvocationOperation *op;
+ op = [[[NSInvocationOperation alloc] initWithTarget:self
+ selector:sel
+ object:args] autorelease];
+ [delegateQueue addOperation:op];
+ } else {
+ [self performSelector:sel
+ onThread:targetThread
+ withObject:args
+ waitUntilDone:NO];
+ }
+ }
+ }
+
+ BOOL didAuth = (args.error == nil);
+ return didAuth;
+}
+
+- (void)invokeCallbackArgs:(GTMOAuth2AuthorizationArgs *)args {
+ // Invoke the callbacks
+ NSError *error = args.error;
+
+ id delegate = args.delegate;
+ SEL sel = args.selector;
+ if (delegate && sel) {
+ NSMutableURLRequest *request = args.request;
+
+ NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&request atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+
+#if NS_BLOCKS_AVAILABLE
+ id handler = args.completionHandler;
+ if (handler) {
+ void (^authCompletionBlock)(NSError *) = handler;
+ authCompletionBlock(error);
+ }
+#endif
+}
+
+- (BOOL)authorizeRequest:(NSMutableURLRequest *)request {
+ // Entry point for synchronous authorization mechanisms
+ GTMOAuth2AuthorizationArgs *args;
+ args = [GTMOAuth2AuthorizationArgs argsWithRequest:request
+ delegate:nil
+ selector:NULL
+ completionHandler:nil
+ thread:[NSThread currentThread]];
+ return [self authorizeRequestImmediateArgs:args];
+}
+
+- (BOOL)canAuthorize {
+ NSString *token = self.refreshToken;
+ if (token == nil) {
+ // For services which do not support refresh tokens, we'll just check
+ // the access token.
+ token = self.authorizationToken;
+ }
+ BOOL canAuth = [token length] > 0;
+ return canAuth;
+}
+
+- (BOOL)shouldRefreshAccessToken {
+ // We should refresh the access token when it's missing or nearly expired
+ // and we have a refresh token
+ BOOL shouldRefresh = NO;
+ NSString *accessToken = self.accessToken;
+ NSString *refreshToken = self.refreshToken;
+ NSString *assertion = self.assertion;
+ NSString *code = self.code;
+
+ BOOL hasRefreshToken = ([refreshToken length] > 0);
+ BOOL hasAccessToken = ([accessToken length] > 0);
+ BOOL hasAssertion = ([assertion length] > 0);
+ BOOL hasCode = ([code length] > 0);
+
+ // Determine if we need to refresh the access token
+ if (hasRefreshToken || hasAssertion || hasCode) {
+ if (!hasAccessToken) {
+ shouldRefresh = YES;
+ } else {
+ // We'll consider the token expired if it expires 60 seconds from now
+ // or earlier
+ NSDate *expirationDate = self.expirationDate;
+ NSTimeInterval timeToExpire = [expirationDate timeIntervalSinceNow];
+ if (expirationDate == nil || timeToExpire < 60.0) {
+ // access token has expired, or will in a few seconds
+ shouldRefresh = YES;
+ }
+ }
+ }
+ return shouldRefresh;
+}
+
+- (void)waitForCompletionWithTimeout:(NSTimeInterval)timeoutInSeconds {
+ // If there is a refresh fetcher pending, wait for it.
+ //
+ // This is only intended for unit test or for use in command-line tools.
+ GTMHTTPFetcher *fetcher = self.refreshFetcher;
+ [fetcher waitForCompletionWithTimeout:timeoutInSeconds];
+}
+
+#pragma mark Token Fetch
+
+- (NSString *)userAgent {
+ NSBundle *bundle = [NSBundle mainBundle];
+ NSString *appID = [bundle bundleIdentifier];
+
+ NSString *version = [bundle objectForInfoDictionaryKey:@"CFBundleShortVersionString"];
+ if (version == nil) {
+ version = [bundle objectForInfoDictionaryKey:@"CFBundleVersion"];
+ }
+
+ if (appID && version) {
+ appID = [appID stringByAppendingFormat:@"/%@", version];
+ }
+
+ NSString *userAgent = @"gtm-oauth2";
+ if (appID) {
+ userAgent = [userAgent stringByAppendingFormat:@" %@", appID];
+ }
+ return userAgent;
+}
+
+- (GTMHTTPFetcher *)beginTokenFetchWithDelegate:(id)delegate
+ didFinishSelector:(SEL)finishedSel {
+
+ NSMutableDictionary *paramsDict = [NSMutableDictionary dictionary];
+
+ NSString *fetchType;
+
+ NSString *refreshToken = self.refreshToken;
+ NSString *code = self.code;
+ NSString *assertion = self.assertion;
+ NSString *grantType = nil;
+
+ if (refreshToken) {
+ // We have a refresh token
+ grantType = @"refresh_token";
+ [paramsDict setObject:refreshToken forKey:@"refresh_token"];
+
+ NSString *refreshScope = self.refreshScope;
+ if ([refreshScope length] > 0) {
+ [paramsDict setObject:refreshScope forKey:@"scope"];
+ }
+
+ fetchType = kGTMOAuth2FetchTypeRefresh;
+ } else if (code) {
+ // We have a code string
+ grantType = @"authorization_code";
+ [paramsDict setObject:code forKey:@"code"];
+
+ NSString *redirectURI = self.redirectURI;
+ if ([redirectURI length] > 0) {
+ [paramsDict setObject:redirectURI forKey:@"redirect_uri"];
+ }
+
+ NSString *scope = self.scope;
+ if ([scope length] > 0) {
+ [paramsDict setObject:scope forKey:@"scope"];
+ }
+
+ fetchType = kGTMOAuth2FetchTypeToken;
+ } else if (assertion) {
+ // We have an assertion string
+ grantType = @"http://oauth.net/grant_type/jwt/1.0/bearer";
+ [paramsDict setObject:assertion forKey:@"assertion"];
+ fetchType = kGTMOAuth2FetchTypeAssertion;
+ } else {
+#if DEBUG
+ NSAssert(0, @"unexpected lack of code or refresh token for fetching");
+#endif
+ return nil;
+ }
+ [paramsDict setObject:grantType forKey:@"grant_type"];
+
+ NSString *clientID = self.clientID;
+ if ([clientID length] > 0) {
+ [paramsDict setObject:clientID forKey:@"client_id"];
+ }
+
+ NSString *clientSecret = self.clientSecret;
+ if ([clientSecret length] > 0) {
+ [paramsDict setObject:clientSecret forKey:@"client_secret"];
+ }
+
+ NSDictionary *additionalParams = self.additionalTokenRequestParameters;
+ if (additionalParams) {
+ [paramsDict addEntriesFromDictionary:additionalParams];
+ }
+ NSDictionary *grantTypeParams =
+ [self.additionalGrantTypeRequestParameters objectForKey:grantType];
+ if (grantTypeParams) {
+ [paramsDict addEntriesFromDictionary:grantTypeParams];
+ }
+
+ NSString *paramStr = [[self class] encodedQueryParametersForDictionary:paramsDict];
+ NSData *paramData = [paramStr dataUsingEncoding:NSUTF8StringEncoding];
+
+ NSURL *tokenURL = self.tokenURL;
+
+ NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:tokenURL];
+ [request setValue:@"application/x-www-form-urlencoded"
+ forHTTPHeaderField:@"Content-Type"];
+
+ NSString *userAgent = [self userAgent];
+ [request setValue:userAgent forHTTPHeaderField:@"User-Agent"];
+
+ GTMHTTPFetcher *fetcher;
+ id <GTMHTTPFetcherServiceProtocol> fetcherService = self.fetcherService;
+ if (fetcherService) {
+ fetcher = [fetcherService fetcherWithRequest:request];
+
+ // Don't use an authorizer for an auth token fetch
+ fetcher.authorizer = nil;
+ } else {
+ fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
+ }
+
+ NSString *const template = (refreshToken ? @"refresh token for %@ %@" : @"fetch tokens for %@ %@");
+ [fetcher setCommentWithFormat:template, [tokenURL host], [self userEmail]];
+ fetcher.postData = paramData;
+ fetcher.retryEnabled = YES;
+ fetcher.maxRetryInterval = 15.0;
+
+ // Fetcher properties will retain the delegate
+ [fetcher setProperty:delegate forKey:kTokenFetchDelegateKey];
+ if (finishedSel) {
+ NSString *selStr = NSStringFromSelector(finishedSel);
+ [fetcher setProperty:selStr forKey:kTokenFetchSelectorKey];
+ }
+
+ if ([fetcher beginFetchWithDelegate:self
+ didFinishSelector:@selector(tokenFetcher:finishedWithData:error:)]) {
+ // Fetch began
+ [self notifyFetchIsRunning:YES fetcher:fetcher type:fetchType];
+ return fetcher;
+ } else {
+ // Failed to start fetching; typically a URL issue
+ NSError *error = [NSError errorWithDomain:kGTMHTTPFetcherStatusDomain
+ code:-1
+ userInfo:nil];
+ [[self class] invokeDelegate:delegate
+ selector:finishedSel
+ object:self
+ object:nil
+ object:error];
+ return nil;
+ }
+}
+
+- (void)tokenFetcher:(GTMHTTPFetcher *)fetcher
+ finishedWithData:(NSData *)data
+ error:(NSError *)error {
+ [self notifyFetchIsRunning:NO fetcher:fetcher type:nil];
+
+ NSDictionary *responseHeaders = [fetcher responseHeaders];
+ NSString *responseType = [responseHeaders valueForKey:@"Content-Type"];
+ BOOL isResponseJSON = [responseType hasPrefix:@"application/json"];
+ BOOL hasData = ([data length] > 0);
+
+ if (error) {
+ // Failed. If the error body is JSON, parse it and add it to the error's
+ // userInfo dictionary.
+ if (hasData) {
+ if (isResponseJSON) {
+ NSDictionary *errorJson = [[self class] dictionaryWithJSONData:data];
+ if ([errorJson count] > 0) {
+#if DEBUG
+ NSLog(@"Error %@\nError data:\n%@", error, errorJson);
+#endif
+ // Add the JSON error body to the userInfo of the error
+ NSMutableDictionary *userInfo;
+ userInfo = [NSMutableDictionary dictionaryWithObject:errorJson
+ forKey:kGTMOAuth2ErrorJSONKey];
+ NSDictionary *prevUserInfo = [error userInfo];
+ if (prevUserInfo) {
+ [userInfo addEntriesFromDictionary:prevUserInfo];
+ }
+ error = [NSError errorWithDomain:[error domain]
+ code:[error code]
+ userInfo:userInfo];
+ }
+ }
+ }
+ } else {
+ // Succeeded; we have the requested token.
+#if DEBUG
+ NSAssert(hasData, @"data missing in token response");
+#endif
+
+ if (hasData) {
+ if (isResponseJSON) {
+ [self setKeysForResponseJSONData:data];
+ } else {
+ // Support for legacy token servers that return form-urlencoded data
+ NSString *dataStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ [self setKeysForResponseString:dataStr];
+ }
+
+#if DEBUG
+ // Watch for token exchanges that return a non-bearer or unlabeled token
+ NSString *tokenType = [self tokenType];
+ if (tokenType == nil
+ || [tokenType caseInsensitiveCompare:@"bearer"] != NSOrderedSame) {
+ NSLog(@"GTMOAuth2: Unexpected token type: %@", tokenType);
+ }
+#endif
+ }
+ }
+
+ id delegate = [fetcher propertyForKey:kTokenFetchDelegateKey];
+ SEL sel = NULL;
+ NSString *selStr = [fetcher propertyForKey:kTokenFetchSelectorKey];
+ if (selStr) sel = NSSelectorFromString(selStr);
+
+ [[self class] invokeDelegate:delegate
+ selector:sel
+ object:self
+ object:fetcher
+ object:error];
+
+ // Prevent a circular reference from retaining the delegate
+ [fetcher setProperty:nil forKey:kTokenFetchDelegateKey];
+}
+
+#pragma mark Fetch Notifications
+
+- (void)notifyFetchIsRunning:(BOOL)isStarting
+ fetcher:(GTMHTTPFetcher *)fetcher
+ type:(NSString *)fetchType {
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+
+ NSString *name = (isStarting ? kGTMOAuth2FetchStarted : kGTMOAuth2FetchStopped);
+ NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
+ fetcher, kGTMOAuth2FetcherKey,
+ fetchType, kGTMOAuth2FetchTypeKey, // fetchType may be nil
+ nil];
+ [nc postNotificationName:name
+ object:self
+ userInfo:dict];
+}
+
+#pragma mark Persistent Response Strings
+
+- (void)setKeysForPersistenceResponseString:(NSString *)str {
+ // All persistence keys can be set directly as if returned by a server
+ [self setKeysForResponseString:str];
+}
+
+// This returns a "response string" that can be passed later to
+// setKeysForResponseString: to reuse an old access token in a new auth object
+- (NSString *)persistenceResponseString {
+ NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity:4];
+
+ NSString *refreshToken = self.refreshToken;
+ NSString *accessToken = nil;
+ if (refreshToken == nil) {
+ // We store the access token only for services that do not support refresh
+ // tokens; otherwise, we assume the access token is too perishable to
+ // be worth storing
+ accessToken = self.accessToken;
+ }
+
+ // Any nil values will not set a dictionary entry
+ [dict setValue:refreshToken forKey:kOAuth2RefreshTokenKey];
+ [dict setValue:accessToken forKey:kOAuth2AccessTokenKey];
+ [dict setValue:self.serviceProvider forKey:kServiceProviderKey];
+ [dict setValue:self.userID forKey:kUserIDKey];
+ [dict setValue:self.userEmail forKey:kUserEmailKey];
+ [dict setValue:self.userEmailIsVerified forKey:kUserEmailIsVerifiedKey];
+ [dict setValue:self.scope forKey:kOAuth2ScopeKey];
+
+ NSString *result = [[self class] encodedQueryParametersForDictionary:dict];
+ return result;
+}
+
+- (BOOL)primeForRefresh {
+ if (self.refreshToken == nil) {
+ // Cannot refresh without a refresh token
+ return NO;
+ }
+ self.accessToken = nil;
+ self.expiresIn = nil;
+ self.expirationDate = nil;
+ self.errorString = nil;
+ return YES;
+}
+
+- (void)reset {
+ // Reset all per-authorization values
+ self.code = nil;
+ self.accessToken = nil;
+ self.refreshToken = nil;
+ self.assertion = nil;
+ self.expiresIn = nil;
+ self.errorString = nil;
+ self.expirationDate = nil;
+ self.userEmail = nil;
+ self.userEmailIsVerified = nil;
+ self.authorizationTokenKey = nil;
+}
+
+#pragma mark Accessors for Response Parameters
+
+- (NSString *)authorizationToken {
+ // The token used for authorization is typically the access token unless
+ // the user has specified that an alternative parameter be used.
+ NSString *authorizationToken;
+ NSString *authTokenKey = self.authorizationTokenKey;
+ if (authTokenKey != nil) {
+ authorizationToken = [self.parameters objectForKey:authTokenKey];
+ } else {
+ authorizationToken = self.accessToken;
+ }
+ return authorizationToken;
+}
+
+- (NSString *)accessToken {
+ return [self.parameters objectForKey:kOAuth2AccessTokenKey];
+}
+
+- (void)setAccessToken:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2AccessTokenKey];
+}
+
+- (NSString *)refreshToken {
+ return [self.parameters objectForKey:kOAuth2RefreshTokenKey];
+}
+
+- (void)setRefreshToken:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2RefreshTokenKey];
+}
+
+- (NSString *)code {
+ return [self.parameters objectForKey:kOAuth2CodeKey];
+}
+
+- (void)setCode:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2CodeKey];
+}
+
+- (NSString *)assertion {
+ return [self.parameters objectForKey:kOAuth2AssertionKey];
+}
+
+- (void)setAssertion:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2AssertionKey];
+}
+
+- (NSString *)refreshScope {
+ return [self.parameters objectForKey:kOAuth2RefreshScopeKey];
+}
+
+- (void)setRefreshScope:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2RefreshScopeKey];
+}
+
+- (NSString *)errorString {
+ return [self.parameters objectForKey:kOAuth2ErrorKey];
+}
+
+- (void)setErrorString:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2ErrorKey];
+}
+
+- (NSString *)tokenType {
+ return [self.parameters objectForKey:kOAuth2TokenTypeKey];
+}
+
+- (void)setTokenType:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2TokenTypeKey];
+}
+
+- (NSString *)scope {
+ return [self.parameters objectForKey:kOAuth2ScopeKey];
+}
+
+- (void)setScope:(NSString *)str {
+ [self.parameters setValue:str forKey:kOAuth2ScopeKey];
+}
+
+- (NSNumber *)expiresIn {
+ return [self.parameters objectForKey:kOAuth2ExpiresInKey];
+}
+
+- (void)setExpiresIn:(NSNumber *)num {
+ [self.parameters setValue:num forKey:kOAuth2ExpiresInKey];
+ [self updateExpirationDate];
+}
+
+- (void)updateExpirationDate {
+ // Update our absolute expiration time to something close to when
+ // the server expects the expiration
+ NSDate *date = nil;
+ NSNumber *expiresIn = self.expiresIn;
+ if (expiresIn) {
+ unsigned long deltaSeconds = [expiresIn unsignedLongValue];
+ if (deltaSeconds > 0) {
+ date = [NSDate dateWithTimeIntervalSinceNow:deltaSeconds];
+ }
+ }
+ self.expirationDate = date;
+}
+
+//
+// Keys custom to this class, not part of OAuth 2
+//
+
+- (NSString *)serviceProvider {
+ return [self.parameters objectForKey:kServiceProviderKey];
+}
+
+- (void)setServiceProvider:(NSString *)str {
+ [self.parameters setValue:str forKey:kServiceProviderKey];
+}
+
+- (NSString *)userID {
+ return [self.parameters objectForKey:kUserIDKey];
+}
+
+- (void)setUserID:(NSString *)str {
+ [self.parameters setValue:str forKey:kUserIDKey];
+}
+
+- (NSString *)userEmail {
+ return [self.parameters objectForKey:kUserEmailKey];
+}
+
+- (void)setUserEmail:(NSString *)str {
+ [self.parameters setValue:str forKey:kUserEmailKey];
+}
+
+- (NSString *)userEmailIsVerified {
+ return [self.parameters objectForKey:kUserEmailIsVerifiedKey];
+}
+
+- (void)setUserEmailIsVerified:(NSString *)str {
+ [self.parameters setValue:str forKey:kUserEmailIsVerifiedKey];
+}
+
+#pragma mark User Properties
+
+- (void)setProperty:(id)obj forKey:(NSString *)key {
+ if (obj == nil) {
+ // User passed in nil, so delete the property
+ [properties_ removeObjectForKey:key];
+ } else {
+ // Be sure the property dictionary exists
+ if (properties_ == nil) {
+ [self setProperties:[NSMutableDictionary dictionary]];
+ }
+ [properties_ setObject:obj forKey:key];
+ }
+}
+
+- (id)propertyForKey:(NSString *)key {
+ id obj = [properties_ objectForKey:key];
+
+ // Be sure the returned pointer has the life of the autorelease pool,
+ // in case self is released immediately
+ return [[obj retain] autorelease];
+}
+
+#pragma mark Utility Routines
+
++ (NSString *)encodedOAuthValueForString:(NSString *)str {
+ CFStringRef originalString = (CFStringRef) str;
+ CFStringRef leaveUnescaped = NULL;
+ CFStringRef forceEscaped = CFSTR("!*'();:@&=+$,/?%#[]");
+
+ CFStringRef escapedStr = NULL;
+ if (str) {
+ escapedStr = CFURLCreateStringByAddingPercentEscapes(kCFAllocatorDefault,
+ originalString,
+ leaveUnescaped,
+ forceEscaped,
+ kCFStringEncodingUTF8);
+ [(id)CFMakeCollectable(escapedStr) autorelease];
+ }
+
+ return (NSString *)escapedStr;
+}
+
++ (NSString *)encodedQueryParametersForDictionary:(NSDictionary *)dict {
+ // Make a string like "cat=fluffy@dog=spot"
+ NSMutableString *result = [NSMutableString string];
+ NSArray *sortedKeys = [[dict allKeys] sortedArrayUsingSelector:@selector(caseInsensitiveCompare:)];
+ NSString *joiner = @"";
+ for (NSString *key in sortedKeys) {
+ NSString *value = [dict objectForKey:key];
+ NSString *encodedValue = [self encodedOAuthValueForString:value];
+ NSString *encodedKey = [self encodedOAuthValueForString:key];
+ [result appendFormat:@"%@%@=%@", joiner, encodedKey, encodedValue];
+ joiner = @"&";
+ }
+ return result;
+}
+
++ (void)invokeDelegate:(id)delegate
+ selector:(SEL)sel
+ object:(id)obj1
+ object:(id)obj2
+ object:(id)obj3 {
+ if (delegate && sel) {
+ NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate];
+ [invocation setArgument:&obj1 atIndex:2];
+ [invocation setArgument:&obj2 atIndex:3];
+ [invocation setArgument:&obj3 atIndex:4];
+ [invocation invoke];
+ }
+}
+
++ (NSString *)unencodedOAuthParameterForString:(NSString *)str {
+ NSString *plainStr = [str stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
+ return plainStr;
+}
+
++ (NSDictionary *)dictionaryWithResponseString:(NSString *)responseStr {
+ // Build a dictionary from a response string of the form
+ // "cat=fluffy&dog=spot". Missing or empty values are considered
+ // empty strings; keys and values are percent-decoded.
+ if (responseStr == nil) return nil;
+
+ NSArray *items = [responseStr componentsSeparatedByString:@"&"];
+
+ NSMutableDictionary *responseDict = [NSMutableDictionary dictionaryWithCapacity:[items count]];
+
+ for (NSString *item in items) {
+ NSString *key = nil;
+ NSString *value = @"";
+
+ NSRange equalsRange = [item rangeOfString:@"="];
+ if (equalsRange.location != NSNotFound) {
+ // The parameter has at least one '='
+ key = [item substringToIndex:equalsRange.location];
+
+ // There are characters after the '='
+ value = [item substringFromIndex:(equalsRange.location + 1)];
+ } else {
+ // The parameter has no '='
+ key = item;
+ }
+
+ NSString *plainKey = [[self class] unencodedOAuthParameterForString:key];
+ NSString *plainValue = [[self class] unencodedOAuthParameterForString:value];
+
+ [responseDict setObject:plainValue forKey:plainKey];
+ }
+
+ return responseDict;
+}
+
++ (NSDictionary *)dictionaryWithResponseData:(NSData *)data {
+ NSString *responseStr = [[[NSString alloc] initWithData:data
+ encoding:NSUTF8StringEncoding] autorelease];
+ NSDictionary *dict = [self dictionaryWithResponseString:responseStr];
+ return dict;
+}
+
++ (NSString *)scopeWithStrings:(NSString *)str, ... {
+ // concatenate the strings, joined by a single space
+ NSString *result = @"";
+ NSString *joiner = @"";
+ if (str) {
+ va_list argList;
+ va_start(argList, str);
+ while (str) {
+ result = [result stringByAppendingFormat:@"%@%@", joiner, str];
+ joiner = @" ";
+ str = va_arg(argList, id);
+ }
+ va_end(argList);
+ }
+ return result;
+}
+
+@end
+
+#endif // GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h
new file mode 100644
index 00000000..ded279bd
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/GTMOAuth2SignIn.h
@@ -0,0 +1,187 @@
+/* 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.
+ */
+
+//
+// This sign-in object opens and closes the web view window as needed for
+// users to sign in. For signing in to Google, it also obtains
+// the authenticated user's email address.
+//
+// Typically, this will be managed for the application by
+// GTMOAuth2ViewControllerTouch or GTMOAuth2WindowController, so this
+// class's interface is interesting only if
+// you are creating your own window controller for sign-in.
+//
+//
+// Delegate methods implemented by the window controller
+//
+// The window controller implements two methods for use by the sign-in object,
+// the webRequestSelector and the finishedSelector:
+//
+// webRequestSelector has a signature matching
+// - (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request
+//
+// The web request selector will be invoked with a request to be displayed, or
+// nil to close the window when the final callback request has been encountered.
+//
+//
+// finishedSelector has a signature matching
+// - (void)signin:(GTMOAuth2SignIn *)signin finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error
+//
+// The finished selector will be invoked when sign-in has completed, except
+// when explicitly canceled by calling cancelSigningIn
+//
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#import <Foundation/Foundation.h>
+#import <SystemConfiguration/SystemConfiguration.h>
+
+// GTMHTTPFetcher brings in GTLDefines/GDataDefines
+#import "GTMHTTPFetcher.h"
+
+#import "GTMOAuth2Authentication.h"
+
+@interface GTMOAuth2SignIn : NSObject {
+ @private
+ GTMOAuth2Authentication *auth_;
+
+ // the endpoint for displaying the sign-in page
+ NSURL *authorizationURL_;
+ NSDictionary *additionalAuthorizationParameters_;
+
+ id delegate_;
+ SEL webRequestSelector_;
+ SEL finishedSelector_;
+
+ BOOL hasHandledCallback_;
+
+ GTMHTTPFetcher *pendingFetcher_;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+ BOOL shouldFetchGoogleUserEmail_;
+ BOOL shouldFetchGoogleUserProfile_;
+ NSDictionary *userProfile_;
+#endif
+
+ SCNetworkReachabilityRef reachabilityRef_;
+ NSTimer *networkLossTimer_;
+ NSTimeInterval networkLossTimeoutInterval_;
+ BOOL hasNotifiedNetworkLoss_;
+
+ id userData_;
+}
+
+@property (nonatomic, retain) GTMOAuth2Authentication *authentication;
+
+@property (nonatomic, retain) NSURL *authorizationURL;
+@property (nonatomic, retain) NSDictionary *additionalAuthorizationParameters;
+
+// The delegate is released when signing in finishes or is cancelled
+@property (nonatomic, retain) id delegate;
+@property (nonatomic, assign) SEL webRequestSelector;
+@property (nonatomic, assign) SEL finishedSelector;
+
+@property (nonatomic, retain) id userData;
+
+// By default, signing in to Google will fetch the user's email, but will not
+// fetch the user's profile.
+//
+// The email is saved in the auth object.
+// The profile is available immediately after sign-in.
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+@property (nonatomic, assign) BOOL shouldFetchGoogleUserEmail;
+@property (nonatomic, assign) BOOL shouldFetchGoogleUserProfile;
+@property (nonatomic, retain, readonly) NSDictionary *userProfile;
+#endif
+
+// The default timeout for an unreachable network during display of the
+// sign-in page is 30 seconds; set this to 0 to have no timeout
+@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
+
+// The delegate is retained until sign-in has completed or been canceled
+//
+// designated initializer
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ delegate:(id)delegate
+ webRequestSelector:(SEL)webRequestSelector
+ finishedSelector:(SEL)finishedSelector;
+
+// A default authentication object for signing in to Google services
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)standardGoogleAuthenticationForScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret;
+#endif
+
+#pragma mark Methods used by the Window Controller
+
+// Start the sequence of fetches and sign-in window display for sign-in
+- (BOOL)startSigningIn;
+
+// Stop any pending fetches, and close the window (but don't call the
+// delegate's finishedSelector)
+- (void)cancelSigningIn;
+
+// Window controllers must tell the sign-in object about any redirect
+// requested by the web view, and any changes in the webview window title
+//
+// If these return YES then the event was handled by the
+// sign-in object (typically by closing the window) and should be ignored by
+// the window controller's web view
+
+- (BOOL)requestRedirectedToRequest:(NSURLRequest *)redirectedRequest;
+- (BOOL)titleChanged:(NSString *)title;
+- (BOOL)cookiesChanged:(NSHTTPCookieStorage *)cookieStorage;
+- (BOOL)loadFailedWithError:(NSError *)error;
+
+// Window controllers must tell the sign-in object if the window was closed
+// prematurely by the user (but not by the sign-in object); this calls the
+// delegate's finishedSelector
+- (void)windowWasClosed;
+
+// Start the sequences for signing in with an authorization code. The
+// authentication must contain an authorization code, otherwise the process
+// will fail.
+- (void)authCodeObtained;
+
+#pragma mark -
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+// Revocation of an authorized token from Google
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
+
+// Create a fetcher for obtaining the user's Google email address or profile,
+// according to the current auth scopes.
+//
+// The auth object must have been created with appropriate scopes.
+//
+// The fetcher's response data can be parsed with NSJSONSerialization.
++ (GTMHTTPFetcher *)userInfoFetcherWithAuth:(GTMOAuth2Authentication *)auth;
+#endif
+
+#pragma mark -
+
+// Standard authentication values
++ (NSString *)nativeClientRedirectURI;
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (NSURL *)googleAuthorizationURL;
++ (NSURL *)googleTokenURL;
++ (NSURL *)googleUserInfoURL;
+#endif
+
+@end
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
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
diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib
new file mode 100644
index 00000000..befc2123
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2Window.xib
@@ -0,0 +1,109 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="4457.6" systemVersion="12E55" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none">
+ <dependencies>
+ <deployment version="1050" defaultVersion="1080" identifier="macosx"/>
+ <plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="4457.6"/>
+ <plugIn identifier="com.apple.WebKitIBPlugin" version="3330"/>
+ </dependencies>
+ <objects>
+ <customObject id="-2" userLabel="File's Owner" customClass="GTMOAuth2WindowController">
+ <connections>
+ <action selector="closeWindow:" destination="17" id="42"/>
+ <outlet property="keychainCheckbox" destination="43" id="46"/>
+ <outlet property="webBackButton" destination="19" id="47"/>
+ <outlet property="webCloseButton" destination="17" id="48"/>
+ <outlet property="webView" destination="5" id="49"/>
+ <outlet property="window" destination="3" id="8"/>
+ </connections>
+ </customObject>
+ <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
+ <customObject id="-3" userLabel="Application"/>
+ <window title="Sign In" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" oneShot="NO" wantsToBeColor="NO" visibleAtLaunch="NO" animationBehavior="default" id="3">
+ <windowStyleMask key="styleMask" titled="YES" closable="YES" resizable="YES"/>
+ <windowPositionMask key="initialPositionMask" leftStrut="YES" bottomStrut="YES"/>
+ <rect key="contentRect" x="74" y="707" width="515" height="419"/>
+ <rect key="screenRect" x="0.0" y="0.0" width="1920" height="1178"/>
+ <value key="minSize" type="size" width="475" height="290"/>
+ <view key="contentView" id="4">
+ <rect key="frame" x="0.0" y="0.0" width="515" height="419"/>
+ <autoresizingMask key="autoresizingMask"/>
+ <subviews>
+ <webView id="5">
+ <rect key="frame" x="0.0" y="20" width="515" height="399"/>
+ <autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
+ <webPreferences key="preferences" defaultFontSize="12" defaultFixedFontSize="12"/>
+ <connections>
+ <action selector="goBack:" destination="19" id="28"/>
+ <action selector="goForward:" destination="26" id="29"/>
+ </connections>
+ </webView>
+ <button id="17">
+ <rect key="frame" x="479" y="0.0" width="16" height="19"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+ <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSStopProgressTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="18">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="system"/>
+ <string key="keyEquivalent" base64-UTF8="YES">
+Gw
+</string>
+ </buttonCell>
+ </button>
+ <button id="19">
+ <rect key="frame" x="437" y="0.0" width="16" height="19"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+ <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoLeftTemplate" imagePosition="overlaps" alignment="center" imageScaling="proportionallyDown" inset="2" id="20">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <binding destination="-2" name="enabled" keyPath="webView.canGoBack" id="31"/>
+ </connections>
+ </button>
+ <button id="26">
+ <rect key="frame" x="456" y="0.0" width="16" height="19"/>
+ <autoresizingMask key="autoresizingMask" flexibleMinX="YES" flexibleMaxY="YES"/>
+ <buttonCell key="cell" type="square" bezelStyle="shadowlessSquare" image="NSGoRightTemplate" imagePosition="only" alignment="center" imageScaling="proportionallyDown" inset="2" id="27">
+ <behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
+ <font key="font" metaFont="system"/>
+ </buttonCell>
+ <connections>
+ <binding destination="-2" name="enabled" keyPath="webView.canGoForward" id="35"/>
+ </connections>
+ </button>
+ <button id="43">
+ <rect key="frame" x="2" y="1" width="429" height="18"/>
+ <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
+ <buttonCell key="cell" type="check" title="Stay signed in to this account" bezelStyle="regularSquare" imagePosition="left" alignment="left" controlSize="small" state="on" inset="2" id="44">
+ <behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
+ <font key="font" metaFont="smallSystem"/>
+ </buttonCell>
+ </button>
+ </subviews>
+ </view>
+ <connections>
+ <outlet property="delegate" destination="-2" id="7"/>
+ </connections>
+ </window>
+ <userDefaultsController id="32"/>
+ </objects>
+ <resources>
+ <image name="NSGoLeftTemplate" width="9" height="9"/>
+ <image name="NSGoRightTemplate" width="9" height="9"/>
+ <image name="NSStopProgressTemplate" width="11" height="11"/>
+ </resources>
+ <classes>
+ <class className="GTMOAuth2WindowController" superclassName="NSWindowController">
+ <source key="sourceIdentifier" type="project" relativePath="./Classes/GTMOAuth2WindowController.h"/>
+ <relationships>
+ <relationship kind="action" name="closeWindow:"/>
+ <relationship kind="outlet" name="completionPlaceholder_"/>
+ <relationship kind="outlet" name="delegate_"/>
+ <relationship kind="outlet" name="keychainCheckbox" candidateClass="NSButton"/>
+ <relationship kind="outlet" name="userData_"/>
+ <relationship kind="outlet" name="webBackButton" candidateClass="NSButton"/>
+ <relationship kind="outlet" name="webCloseButton" candidateClass="NSButton"/>
+ <relationship kind="outlet" name="webView" candidateClass="WebView"/>
+ </relationships>
+ </class>
+ </classes>
+</document> \ No newline at end of file
diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h
new file mode 100644
index 00000000..9ff89b70
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.h
@@ -0,0 +1,332 @@
+/* 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.
+ */
+
+// GTMOAuth2WindowController
+//
+// This window controller for Mac handles sign-in via OAuth2 to Google or
+// other services.
+//
+// This controller is not reusable; create a new instance of this controller
+// every time the user will sign in.
+//
+// Sample usage for signing in to a Google service:
+//
+// static NSString *const kKeychainItemName = @”My App: Google Plus”;
+// NSString *scope = @"https://www.googleapis.com/auth/plus.me";
+//
+//
+// GTMOAuth2WindowController *windowController;
+// windowController = [[[GTMOAuth2WindowController alloc] initWithScope:scope
+// clientID:clientID
+// clientSecret:clientSecret
+// keychainItemName:kKeychainItemName
+// resourceBundle:nil] autorelease];
+//
+// [windowController signInSheetModalForWindow:mMainWindow
+// delegate:self
+// finishedSelector:@selector(windowController:finishedWithAuth:error:)];
+//
+// The finished selector should have a signature matching this:
+//
+// - (void)windowController:(GTMOAuth2WindowController *)windowController
+// finishedWithAuth:(GTMOAuth2Authentication *)auth
+// error:(NSError *)error {
+// if (error != nil) {
+// // sign in failed
+// } else {
+// // sign in succeeded
+// //
+// // with the GTL library, pass the authentication to the service object,
+// // like
+// // [[self contactService] setAuthorizer:auth];
+// //
+// // or use it to sign a request directly, like
+// // BOOL isAuthorizing = [self authorizeRequest:request
+// // delegate:self
+// // didFinishSelector:@selector(auth:finishedWithError:)];
+// }
+// }
+//
+// To sign in to services other than Google, use the longer init method,
+// as shown in the sample application
+//
+// If the network connection is lost for more than 30 seconds while the sign-in
+// html is displayed, the notification kGTLOAuthNetworkLost will be sent.
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#include <Foundation/Foundation.h>
+
+#if !TARGET_OS_IPHONE
+
+#import <Cocoa/Cocoa.h>
+#import <WebKit/WebKit.h>
+
+// GTMHTTPFetcher.h brings in GTLDefines/GDataDefines
+#import "GTMHTTPFetcher.h"
+
+#import "GTMOAuth2SignIn.h"
+#import "GTMOAuth2Authentication.h"
+#import "GTMHTTPFetchHistory.h" // for GTMCookieStorage
+
+@class GTMOAuth2SignIn;
+
+@interface GTMOAuth2WindowController : NSWindowController {
+ @private
+ // IBOutlets
+ NSButton *keychainCheckbox_;
+ WebView *webView_;
+ NSButton *webCloseButton_;
+ NSButton *webBackButton_;
+
+ // the object responsible for the sign-in networking sequence; it holds
+ // onto the authentication object as well
+ GTMOAuth2SignIn *signIn_;
+
+ // the page request to load when awakeFromNib occurs
+ NSURLRequest *initialRequest_;
+
+ // local storage for WebKit cookies so they're not shared with Safari
+ GTMCookieStorage *cookieStorage_;
+
+ // the user we're calling back
+ //
+ // the delegate is retained only until the callback is invoked
+ // or the sign-in is canceled
+ id delegate_;
+ SEL finishedSelector_;
+
+#if NS_BLOCKS_AVAILABLE
+ void (^completionBlock_)(GTMOAuth2Authentication *, NSError *);
+#elif !__LP64__
+ // placeholders: for 32-bit builds, keep the size of the object's ivar section
+ // the same with and without blocks
+#ifndef __clang_analyzer__
+ id completionPlaceholder_;
+#endif
+#endif
+
+ // flag allowing application to quit during display of sign-in sheet on 10.6
+ // and later
+ BOOL shouldAllowApplicationTermination_;
+
+ // delegate method for handling URLs to be opened in external windows
+ SEL externalRequestSelector_;
+
+ BOOL isWindowShown_;
+
+ // paranoid flag to ensure we only close once during the sign-in sequence
+ BOOL hasDoneFinalRedirect_;
+
+ // paranoid flag to ensure we only call the user back once
+ BOOL hasCalledFinished_;
+
+ // if non-nil, we display as a sheet on the specified window
+ NSWindow *sheetModalForWindow_;
+
+ // if non-empty, the name of the application and service used for the
+ // keychain item
+ NSString *keychainItemName_;
+
+ // if non-nil, the html string to be displayed immediately upon opening
+ // of the web view
+ NSString *initialHTMLString_;
+
+ // if true, we allow default WebView handling of cookies, so the
+ // same user remains signed in each time the dialog is displayed
+ BOOL shouldPersistUser_;
+
+ // user-defined data
+ id userData_;
+ NSMutableDictionary *properties_;
+}
+
+// User interface elements
+@property (nonatomic, assign) IBOutlet NSButton *keychainCheckbox;
+@property (nonatomic, assign) IBOutlet WebView *webView;
+@property (nonatomic, assign) IBOutlet NSButton *webCloseButton;
+@property (nonatomic, assign) IBOutlet NSButton *webBackButton;
+
+// The application and service name to use for saving the auth tokens
+// to the keychain
+@property (nonatomic, copy) NSString *keychainItemName;
+
+// If true, the sign-in will remember which user was last signed in
+//
+// Defaults to false, so showing the sign-in window will always ask for
+// the username and password, rather than skip to the grant authorization
+// page. During development, it may be convenient to set this to true
+// to speed up signing in.
+@property (nonatomic, assign) BOOL shouldPersistUser;
+
+// Optional html string displayed immediately upon opening the web view
+//
+// This string is visible just until the sign-in web page loads, and
+// may be used for a "Loading..." type of message
+@property (nonatomic, copy) NSString *initialHTMLString;
+
+// The default timeout for an unreachable network during display of the
+// sign-in page is 30 seconds, after which the notification
+// kGTLOAuthNetworkLost is sent; set this to 0 to have no timeout
+@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
+
+// On 10.6 and later, the sheet can allow application termination by calling
+// NSWindow's setPreventsApplicationTerminationWhenModal:
+@property (nonatomic, assign) BOOL shouldAllowApplicationTermination;
+
+// Selector for a delegate method to handle requests sent to an external
+// browser.
+//
+// Selector should have a signature matching
+// - (void)windowController:(GTMOAuth2WindowController *)controller
+// opensRequest:(NSURLRequest *)request;
+//
+// The controller's default behavior is to use NSWorkspace's openURL:
+@property (nonatomic, assign) SEL externalRequestSelector;
+
+// The underlying object to hold authentication tokens and authorize http
+// requests
+@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication;
+
+// The underlying object which performs the sign-in networking sequence
+@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn;
+
+// Any arbitrary data object the user would like the controller to retain
+@property (nonatomic, retain) id userData;
+
+// Stored property values are retained for the convenience of the caller
+- (void)setProperty:(id)obj forKey:(NSString *)key;
+- (id)propertyForKey:(NSString *)key;
+
+@property (nonatomic, retain) NSDictionary *properties;
+
+- (IBAction)closeWindow:(id)sender;
+
+// Create a controller for authenticating to Google services
+//
+// scope is the requested scope of authorization
+// (like "http://www.google.com/m8/feeds")
+//
+// keychainItemName is used for storing the token on the keychain,
+// and is required for the "remember for later" checkbox to be shown;
+// keychainItemName should be like "My Application: Google Contacts"
+// (or set to nil if no persistent keychain storage is desired)
+//
+// resourceBundle may be nil if the window is in the main bundle's nib
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName // may be nil
+ resourceBundle:(NSBundle *)bundle; // may be nil
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle;
+#endif
+
+// Create a controller for authenticating to non-Google services, taking
+// explicit endpoint URLs and an authentication object
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName // may be nil
+ resourceBundle:(NSBundle *)bundle; // may be nil
+
+// This is the designated initializer
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle;
+
+// Entry point to begin displaying the sign-in window
+//
+// the finished selector should have a signature matching
+// - (void)windowController:(GTMOAuth2WindowController *)windowController
+// finishedWithAuth:(GTMOAuth2Authentication *)auth
+// error:(NSError *)error {
+//
+// Once the finished method has been invoked with no error, the auth object
+// may be used to authorize requests (refreshing the access token, if necessary,
+// and adding the auth header) like:
+//
+// [authorizer authorizeRequest:myNSMutableURLRequest]
+// delegate:self
+// didFinishSelector:@selector(auth:finishedWithError:)];
+//
+// or can be stored in a GTL service object like
+// GTLServiceGoogleContact *service = [self contactService];
+// [service setAuthorizer:auth];
+//
+// The delegate is retained only until the finished selector is invoked or
+// the sign-in is canceled
+- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+#if NS_BLOCKS_AVAILABLE
+- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
+ completionHandler:(void (^)(GTMOAuth2Authentication *auth, NSError *error))handler;
+#endif
+
+- (void)cancelSigningIn;
+
+// Subclasses may override authNibName to specify a custom name
++ (NSString *)authNibName;
+
+// apps may replace the sign-in class with their own subclass of it
++ (Class)signInClass;
++ (void)setSignInClass:(Class)theClass;
+
+// Revocation of an authorized token from Google
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
+#endif
+
+// Keychain
+//
+// The keychain checkbox is shown if the keychain application service
+// name (typically set in the initWithScope: method) is non-empty
+//
+
+// Create an authentication object for Google services from the access
+// token and secret stored in the keychain; if no token is available, return
+// an unauthorized auth object
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret;
+#endif
+
+// Add tokens from the keychain, if available, to the authentication object
+//
+// returns YES if the authentication object was authorized from the keychain
++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth;
+
+// Method for deleting the stored access token and secret, useful for "signing
+// out"
++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName;
+
+// Method for saving the stored access token and secret; typically, this method
+// is used only by the window controller
++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth;
+@end
+
+#endif // #if !TARGET_OS_IPHONE
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m
new file mode 100644
index 00000000..948975bf
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Mac/GTMOAuth2WindowController.m
@@ -0,0 +1,727 @@
+/* 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.
+ */
+
+#import <Foundation/Foundation.h>
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#if !TARGET_OS_IPHONE
+
+#import "GTMOAuth2WindowController.h"
+
+@interface GTMOAuth2WindowController ()
+@property (nonatomic, retain) GTMOAuth2SignIn *signIn;
+@property (nonatomic, copy) NSURLRequest *initialRequest;
+@property (nonatomic, retain) GTMCookieStorage *cookieStorage;
+@property (nonatomic, retain) NSWindow *sheetModalForWindow;
+
+- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil;
+- (void)setupSheetTerminationHandling;
+- (void)destroyWindow;
+- (void)handlePrematureWindowClose;
+- (BOOL)shouldUseKeychain;
+- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
+- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error;
+- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo;
+
+- (void)handleCookiesForResponse:(NSURLResponse *)response;
+- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request;
+@end
+
+const char *kKeychainAccountName = "OAuth";
+
+@implementation GTMOAuth2WindowController
+
+// IBOutlets
+@synthesize keychainCheckbox = keychainCheckbox_,
+ webView = webView_,
+ webCloseButton = webCloseButton_,
+ webBackButton = webBackButton_;
+
+// regular ivars
+@synthesize signIn = signIn_,
+ initialRequest = initialRequest_,
+ cookieStorage = cookieStorage_,
+ sheetModalForWindow = sheetModalForWindow_,
+ keychainItemName = keychainItemName_,
+ initialHTMLString = initialHTMLString_,
+ shouldAllowApplicationTermination = shouldAllowApplicationTermination_,
+ externalRequestSelector = externalRequestSelector_,
+ shouldPersistUser = shouldPersistUser_,
+ userData = userData_,
+ properties = properties_;
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+// Create a controller for authenticating to Google services
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle {
+ return [[[self alloc] initWithScope:scope
+ clientID:clientID
+ clientSecret:clientSecret
+ keychainItemName:keychainItemName
+ resourceBundle:bundle] autorelease];
+}
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle {
+ Class signInClass = [[self class] signInClass];
+ GTMOAuth2Authentication *auth;
+ auth = [signInClass standardGoogleAuthenticationForScope:scope
+ clientID:clientID
+ clientSecret:clientSecret];
+ NSURL *authorizationURL = [signInClass googleAuthorizationURL];
+ return [self initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ resourceBundle:bundle];
+}
+#endif
+
+// Create a controller for authenticating to any service
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle {
+ return [[[self alloc] initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ resourceBundle:bundle] autorelease];
+}
+
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ resourceBundle:(NSBundle *)bundle {
+ if (bundle == nil) {
+ bundle = [NSBundle mainBundle];
+ }
+
+ NSString *nibName = [[self class] authNibName];
+ NSString *nibPath = [bundle pathForResource:nibName
+ ofType:@"nib"];
+ self = [super initWithWindowNibPath:nibPath
+ owner:self];
+ if (self != nil) {
+ // use the supplied auth and OAuth endpoint URLs
+ Class signInClass = [[self class] signInClass];
+ signIn_ = [[signInClass alloc] initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ delegate:self
+ webRequestSelector:@selector(signIn:displayRequest:)
+ finishedSelector:@selector(signIn:finishedWithAuth:error:)];
+ keychainItemName_ = [keychainItemName copy];
+
+ // create local, temporary storage for WebKit cookies
+ cookieStorage_ = [[GTMCookieStorage alloc] init];
+ }
+ return self;
+}
+
+- (void)dealloc {
+ [signIn_ release];
+ [initialRequest_ release];
+ [cookieStorage_ release];
+ [delegate_ release];
+#if NS_BLOCKS_AVAILABLE
+ [completionBlock_ release];
+#endif
+ [sheetModalForWindow_ release];
+ [keychainItemName_ release];
+ [initialHTMLString_ release];
+ [userData_ release];
+ [properties_ release];
+
+ [super dealloc];
+}
+
+- (void)awakeFromNib {
+ // load the requested initial sign-in page
+ [self.webView setResourceLoadDelegate:self];
+ [self.webView setPolicyDelegate:self];
+
+ // the app may prefer some html other than blank white to be displayed
+ // before the sign-in web page loads
+ NSString *html = self.initialHTMLString;
+ if ([html length] > 0) {
+ [[self.webView mainFrame] loadHTMLString:html baseURL:nil];
+ }
+
+ // hide the keychain checkbox if we're not supporting keychain
+ BOOL hideKeychainCheckbox = ![self shouldUseKeychain];
+
+ const NSTimeInterval kJanuary2011 = 1293840000;
+ BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
+ if (isDateValid) {
+ // start the asynchronous load of the sign-in web page
+ [[self.webView mainFrame] performSelector:@selector(loadRequest:)
+ withObject:self.initialRequest
+ afterDelay:0.01
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
+ } else {
+ // clock date is invalid, so signing in would fail with an unhelpful error
+ // from the server. Warn the user in an html string showing a watch icon,
+ // question mark, and the system date and time. Hopefully this will clue
+ // in brighter users, or at least let them make a useful screenshot to show
+ // to developers.
+ //
+ // Even better is for apps to check the system clock and show some more
+ // helpful, localized instructions for users; this is really a fallback.
+ NSString *const htmlTemplate = @"<html><body><div align=center><font size='7'>"
+ @"&#x231A; ?<br><i>System Clock Incorrect</i><br>%@"
+ @"</font></div></body></html>";
+ NSString *errHTML = [NSString stringWithFormat:htmlTemplate, [NSDate date]];
+
+ [[webView_ mainFrame] loadHTMLString:errHTML baseURL:nil];
+ hideKeychainCheckbox = YES;
+ }
+
+#if DEBUG
+ // Verify that Javascript is enabled
+ BOOL hasJS = [[webView_ preferences] isJavaScriptEnabled];
+ NSAssert(hasJS, @"GTMOAuth2: Javascript is required");
+#endif
+
+ [keychainCheckbox_ setHidden:hideKeychainCheckbox];
+}
+
++ (NSString *)authNibName {
+ // subclasses may override this to specify a custom nib name
+ return @"GTMOAuth2Window";
+}
+
+#pragma mark -
+
+- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+ // check the selector on debug builds
+ GTMAssertSelectorNilOrImplementedWithArgs(delegate, finishedSelector,
+ @encode(GTMOAuth2WindowController *), @encode(GTMOAuth2Authentication *),
+ @encode(NSError *), 0);
+
+ delegate_ = [delegate retain];
+ finishedSelector_ = finishedSelector;
+
+ [self signInCommonForWindow:parentWindowOrNil];
+}
+
+#if NS_BLOCKS_AVAILABLE
+- (void)signInSheetModalForWindow:(NSWindow *)parentWindowOrNil
+ completionHandler:(void (^)(GTMOAuth2Authentication *, NSError *))handler {
+ completionBlock_ = [handler copy];
+
+ [self signInCommonForWindow:parentWindowOrNil];
+}
+#endif
+
+- (void)signInCommonForWindow:(NSWindow *)parentWindowOrNil {
+ self.sheetModalForWindow = parentWindowOrNil;
+ hasDoneFinalRedirect_ = NO;
+ hasCalledFinished_ = NO;
+
+ [self.signIn startSigningIn];
+}
+
+- (void)cancelSigningIn {
+ // The user has explicitly asked us to cancel signing in
+ // (so no further callback is required)
+ hasCalledFinished_ = YES;
+
+ [delegate_ autorelease];
+ delegate_ = nil;
+
+#if NS_BLOCKS_AVAILABLE
+ [completionBlock_ autorelease];
+ completionBlock_ = nil;
+#endif
+
+ // The signIn object's cancel method will close the window
+ [self.signIn cancelSigningIn];
+ hasDoneFinalRedirect_ = YES;
+}
+
+- (IBAction)closeWindow:(id)sender {
+ // dismiss the window/sheet before we call back the client
+ [self destroyWindow];
+ [self handlePrematureWindowClose];
+}
+
+#pragma mark SignIn callbacks
+
+- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
+ // this is the signIn object's webRequest method, telling the controller
+ // to either display the request in the webview, or close the window
+ //
+ // All web requests and all window closing goes through this routine
+
+#if DEBUG
+ if ((isWindowShown_ && request != nil)
+ || (!isWindowShown_ && request == nil)) {
+ NSLog(@"Window state unexpected for request %@", [request URL]);
+ return;
+ }
+#endif
+
+ if (request != nil) {
+ // display the request
+ self.initialRequest = request;
+
+ NSWindow *parentWindow = self.sheetModalForWindow;
+ if (parentWindow) {
+ [self setupSheetTerminationHandling];
+
+ NSWindow *sheet = [self window];
+ [NSApp beginSheet:sheet
+ modalForWindow:parentWindow
+ modalDelegate:self
+ didEndSelector:@selector(sheetDidEnd:returnCode:contextInfo:)
+ contextInfo:nil];
+ } else {
+ // modeless
+ [self showWindow:self];
+ }
+ isWindowShown_ = YES;
+ } else {
+ // request was nil
+ [self destroyWindow];
+ }
+}
+
+- (void)setupSheetTerminationHandling {
+ NSWindow *sheet = [self window];
+
+ SEL sel = @selector(setPreventsApplicationTerminationWhenModal:);
+ if ([sheet respondsToSelector:sel]) {
+ // setPreventsApplicationTerminationWhenModal is available in NSWindow
+ // on 10.6 and later
+ BOOL boolVal = !self.shouldAllowApplicationTermination;
+ NSMethodSignature *sig = [sheet methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:sheet];
+ [invocation setArgument:&boolVal atIndex:2];
+ [invocation invoke];
+ }
+}
+
+- (void)destroyWindow {
+ // no request; close the window
+
+ // Avoid more callbacks after the close happens, as the window
+ // controller may be gone.
+ [self.webView stopLoading:nil];
+
+ NSWindow *parentWindow = self.sheetModalForWindow;
+ if (parentWindow) {
+ [NSApp endSheet:[self window]];
+ } else {
+ // defer closing the window, in case we're responding to some window event
+ [[self window] performSelector:@selector(close)
+ withObject:nil
+ afterDelay:0.1
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
+
+ }
+ isWindowShown_ = NO;
+}
+
+- (void)handlePrematureWindowClose {
+ if (!hasDoneFinalRedirect_) {
+ // tell the sign-in object to tell the user's finished method
+ // that we're done
+ [self.signIn windowWasClosed];
+ hasDoneFinalRedirect_ = YES;
+ }
+}
+
+- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo {
+ [sheet orderOut:self];
+
+ self.sheetModalForWindow = nil;
+}
+
+- (void)signIn:(GTMOAuth2SignIn *)signIn finishedWithAuth:(GTMOAuth2Authentication *)auth error:(NSError *)error {
+ if (!hasCalledFinished_) {
+ hasCalledFinished_ = YES;
+
+ if (error == nil) {
+ BOOL shouldUseKeychain = [self shouldUseKeychain];
+ if (shouldUseKeychain) {
+ BOOL canAuthorize = auth.canAuthorize;
+ BOOL isKeychainChecked = ([keychainCheckbox_ state] == NSOnState);
+
+ NSString *keychainItemName = self.keychainItemName;
+
+ if (isKeychainChecked && canAuthorize) {
+ // save the auth params in the keychain
+ [[self class] saveAuthToKeychainForName:keychainItemName
+ authentication:auth];
+ } else {
+ // remove the auth params from the keychain
+ [[self class] removeAuthFromKeychainForName:keychainItemName];
+ }
+ }
+ }
+
+ if (delegate_ && finishedSelector_) {
+ SEL sel = finishedSelector_;
+ NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate_];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&auth atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+
+ [delegate_ autorelease];
+ delegate_ = nil;
+
+#if NS_BLOCKS_AVAILABLE
+ if (completionBlock_) {
+ completionBlock_(auth, error);
+
+ // release the block here to avoid a retain loop on the controller
+ [completionBlock_ autorelease];
+ completionBlock_ = nil;
+ }
+#endif
+ }
+}
+
+static Class gSignInClass = Nil;
+
++ (Class)signInClass {
+ if (gSignInClass == Nil) {
+ gSignInClass = [GTMOAuth2SignIn class];
+ }
+ return gSignInClass;
+}
+
++ (void)setSignInClass:(Class)theClass {
+ gSignInClass = theClass;
+}
+
+#pragma mark Token Revocation
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
+ [[self signInClass] revokeTokenForGoogleAuthentication:auth];
+}
+#endif
+
+#pragma mark WebView methods
+
+- (NSURLRequest *)webView:(WebView *)sender resource:(id)identifier willSendRequest:(NSURLRequest *)request redirectResponse:(NSURLResponse *)redirectResponse fromDataSource:(WebDataSource *)dataSource {
+ // override WebKit's cookie storage with our own to avoid cookie persistence
+ // across sign-ins and interaction with the Safari browser's sign-in state
+ [self handleCookiesForResponse:redirectResponse];
+ request = [self addCookiesToRequest:request];
+
+ if (!hasDoneFinalRedirect_) {
+ hasDoneFinalRedirect_ = [self.signIn requestRedirectedToRequest:request];
+ if (hasDoneFinalRedirect_) {
+ // signIn has told the window to close
+ return nil;
+ }
+ }
+ return request;
+}
+
+- (void)webView:(WebView *)sender resource:(id)identifier didReceiveResponse:(NSURLResponse *)response fromDataSource:(WebDataSource *)dataSource {
+ // override WebKit's cookie storage with our own
+ [self handleCookiesForResponse:response];
+}
+
+- (void)webView:(WebView *)sender resource:(id)identifier didFinishLoadingFromDataSource:(WebDataSource *)dataSource {
+ NSString *title = [sender stringByEvaluatingJavaScriptFromString:@"document.title"];
+ if ([title length] > 0) {
+ [self.signIn titleChanged:title];
+ }
+
+ [signIn_ cookiesChanged:(NSHTTPCookieStorage *)cookieStorage_];
+}
+
+- (void)webView:(WebView *)sender resource:(id)identifier didFailLoadingWithError:(NSError *)error fromDataSource:(WebDataSource *)dataSource {
+ [self.signIn loadFailedWithError:error];
+}
+
+- (void)windowWillClose:(NSNotification *)note {
+ if (isWindowShown_) {
+ [self handlePrematureWindowClose];
+ }
+ isWindowShown_ = NO;
+}
+
+- (void)webView:(WebView *)webView
+decidePolicyForNewWindowAction:(NSDictionary *)actionInformation
+ request:(NSURLRequest *)request
+ newFrameName:(NSString *)frameName
+decisionListener:(id<WebPolicyDecisionListener>)listener {
+ SEL sel = self.externalRequestSelector;
+ if (sel) {
+ [delegate_ performSelector:sel
+ withObject:self
+ withObject:request];
+ } else {
+ // default behavior is to open the URL in NSWorkspace's default browser
+ NSURL *url = [request URL];
+ [[NSWorkspace sharedWorkspace] openURL:url];
+ }
+ [listener ignore];
+}
+
+#pragma mark Cookie management
+
+// Rather than let the WebView use Safari's default cookie storage, we intercept
+// requests and response to segregate and later discard cookies from signing in.
+//
+// This allows the application to actually sign out by discarding the auth token
+// rather than the user being kept signed in by the cookies.
+
+- (void)handleCookiesForResponse:(NSURLResponse *)response {
+ if (self.shouldPersistUser) {
+ // we'll let WebKit handle the cookies; they'll persist across apps
+ // and across runs of this app
+ return;
+ }
+
+ if ([response respondsToSelector:@selector(allHeaderFields)]) {
+ // grab the cookies from the header as NSHTTPCookies and store them locally
+ NSDictionary *headers = [(NSHTTPURLResponse *)response allHeaderFields];
+ if (headers) {
+ NSURL *url = [response URL];
+ NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:headers
+ forURL:url];
+ if ([cookies count] > 0) {
+ [cookieStorage_ setCookies:cookies];
+ }
+ }
+ }
+}
+
+- (NSURLRequest *)addCookiesToRequest:(NSURLRequest *)request {
+ if (self.shouldPersistUser) {
+ // we'll let WebKit handle the cookies; they'll persist across apps
+ // and across runs of this app
+ return request;
+ }
+
+ // override WebKit's usual automatic storage of cookies
+ NSMutableURLRequest *mutableRequest = [[request mutableCopy] autorelease];
+ [mutableRequest setHTTPShouldHandleCookies:NO];
+
+ // add our locally-stored cookies for this URL, if any
+ NSArray *cookies = [cookieStorage_ cookiesForURL:[request URL]];
+ if ([cookies count] > 0) {
+ NSDictionary *headers = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
+ NSString *cookieHeader = [headers objectForKey:@"Cookie"];
+ if (cookieHeader) {
+ [mutableRequest setValue:cookieHeader forHTTPHeaderField:@"Cookie"];
+ }
+ }
+ return mutableRequest;
+}
+
+#pragma mark Keychain support
+
++ (NSString *)prefsKeyForName:(NSString *)keychainItemName {
+ NSString *result = [@"OAuth2: " stringByAppendingString:keychainItemName];
+ return result;
+}
+
++ (BOOL)saveAuthToKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth {
+
+ [self removeAuthFromKeychainForName:keychainItemName];
+
+ // don't save unless we have a token that can really authorize requests
+ if (!auth.canAuthorize) return NO;
+
+ // make a response string containing the values we want to save
+ NSString *password = [auth persistenceResponseString];
+
+ SecKeychainRef defaultKeychain = NULL;
+ SecKeychainItemRef *dontWantItemRef= NULL;
+ const char *utf8ServiceName = [keychainItemName UTF8String];
+ const char *utf8Password = [password UTF8String];
+
+ OSStatus err = SecKeychainAddGenericPassword(defaultKeychain,
+ (UInt32) strlen(utf8ServiceName), utf8ServiceName,
+ (UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
+ (UInt32) strlen(utf8Password), utf8Password,
+ dontWantItemRef);
+ BOOL didSucceed = (err == noErr);
+ if (didSucceed) {
+ // write to preferences that we have a keychain item (so we know later
+ // that we can read from the keychain without raising a permissions dialog)
+ NSString *prefKey = [self prefsKeyForName:keychainItemName];
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ [defaults setBool:YES forKey:prefKey];
+ }
+
+ return didSucceed;
+}
+
++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
+
+ SecKeychainRef defaultKeychain = NULL;
+ SecKeychainItemRef itemRef = NULL;
+ const char *utf8ServiceName = [keychainItemName UTF8String];
+
+ // we don't really care about the password here, we just want to
+ // get the SecKeychainItemRef so we can delete it.
+ OSStatus err = SecKeychainFindGenericPassword (defaultKeychain,
+ (UInt32) strlen(utf8ServiceName), utf8ServiceName,
+ (UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
+ 0, NULL, // ignore password
+ &itemRef);
+ if (err != noErr) {
+ // failure to find is success
+ return YES;
+ } else {
+ // found something, so delete it
+ err = SecKeychainItemDelete(itemRef);
+ CFRelease(itemRef);
+
+ // remove our preference key
+ NSString *prefKey = [self prefsKeyForName:keychainItemName];
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ [defaults removeObjectForKey:prefKey];
+
+ return (err == noErr);
+ }
+}
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret {
+ Class signInClass = [self signInClass];
+ NSURL *tokenURL = [signInClass googleTokenURL];
+ NSString *redirectURI = [signInClass nativeClientRedirectURI];
+
+ GTMOAuth2Authentication *auth;
+ auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
+ tokenURL:tokenURL
+ redirectURI:redirectURI
+ clientID:clientID
+ clientSecret:clientSecret];
+
+ [GTMOAuth2WindowController authorizeFromKeychainForName:keychainItemName
+ authentication:auth];
+ return auth;
+}
+#endif
+
++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)newAuth {
+ [newAuth setAccessToken:nil];
+
+ // before accessing the keychain, check preferences to verify that we've
+ // previously saved a token to the keychain (so we don't needlessly raise
+ // a keychain access permission dialog)
+ NSString *prefKey = [self prefsKeyForName:keychainItemName];
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ BOOL flag = [defaults boolForKey:prefKey];
+ if (!flag) {
+ return NO;
+ }
+
+ BOOL didGetTokens = NO;
+
+ SecKeychainRef defaultKeychain = NULL;
+ const char *utf8ServiceName = [keychainItemName UTF8String];
+ SecKeychainItemRef *dontWantItemRef = NULL;
+
+ void *passwordBuff = NULL;
+ UInt32 passwordBuffLength = 0;
+
+ OSStatus err = SecKeychainFindGenericPassword(defaultKeychain,
+ (UInt32) strlen(utf8ServiceName), utf8ServiceName,
+ (UInt32) strlen(kKeychainAccountName), kKeychainAccountName,
+ &passwordBuffLength, &passwordBuff,
+ dontWantItemRef);
+ if (err == noErr && passwordBuff != NULL) {
+
+ NSString *password = [[[NSString alloc] initWithBytes:passwordBuff
+ length:passwordBuffLength
+ encoding:NSUTF8StringEncoding] autorelease];
+
+ // free the password buffer that was allocated above
+ SecKeychainItemFreeContent(NULL, passwordBuff);
+
+ if (password != nil) {
+ [newAuth setKeysForResponseString:password];
+ didGetTokens = YES;
+ }
+ }
+ return didGetTokens;
+}
+
+#pragma mark User Properties
+
+- (void)setProperty:(id)obj forKey:(NSString *)key {
+ if (obj == nil) {
+ // User passed in nil, so delete the property
+ [properties_ removeObjectForKey:key];
+ } else {
+ // Be sure the property dictionary exists
+ if (properties_ == nil) {
+ [self setProperties:[NSMutableDictionary dictionary]];
+ }
+ [properties_ setObject:obj forKey:key];
+ }
+}
+
+- (id)propertyForKey:(NSString *)key {
+ id obj = [properties_ objectForKey:key];
+
+ // Be sure the returned pointer has the life of the autorelease pool,
+ // in case self is released immediately
+ return [[obj retain] autorelease];
+}
+
+#pragma mark Accessors
+
+- (GTMOAuth2Authentication *)authentication {
+ return self.signIn.authentication;
+}
+
+- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
+ self.signIn.networkLossTimeoutInterval = val;
+}
+
+- (NSTimeInterval)networkLossTimeoutInterval {
+ return self.signIn.networkLossTimeoutInterval;
+}
+
+- (BOOL)shouldUseKeychain {
+ NSString *name = self.keychainItemName;
+ return ([name length] > 0);
+}
+
+@end
+
+#endif // #if !TARGET_OS_IPHONE
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h
new file mode 100644
index 00000000..33adbf71
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.h
@@ -0,0 +1,376 @@
+/* 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.
+ */
+
+//
+// GTMOAuth2ViewControllerTouch.h
+//
+// This view controller for iPhone handles sign-in via OAuth to Google or
+// other services.
+//
+// This controller is not reusable; create a new instance of this controller
+// every time the user will sign in.
+//
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#import <Foundation/Foundation.h>
+
+#if TARGET_OS_IPHONE
+
+#import <UIKit/UIKit.h>
+
+#import "GTMOAuth2Authentication.h"
+
+#undef _EXTERN
+#undef _INITIALIZE_AS
+#ifdef GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS
+#define _EXTERN
+#define _INITIALIZE_AS(x) =x
+#else
+#define _EXTERN extern
+#define _INITIALIZE_AS(x)
+#endif
+
+_EXTERN NSString* const kGTMOAuth2KeychainErrorDomain _INITIALIZE_AS(@"com.google.GTMOAuthKeychain");
+
+@class GTMOAuth2SignIn;
+@class GTMOAuth2ViewControllerTouch;
+
+typedef void (^GTMOAuth2ViewControllerCompletionHandler)(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error);
+
+@interface GTMOAuth2ViewControllerTouch : UIViewController<UINavigationControllerDelegate, UIWebViewDelegate> {
+ @private
+ UIButton *backButton_;
+ UIButton *forwardButton_;
+ UIActivityIndicatorView *initialActivityIndicator_;
+ UIView *navButtonsView_;
+ UIBarButtonItem *rightBarButtonItem_;
+ UIWebView *webView_;
+
+ // The object responsible for the sign-in networking sequence; it holds
+ // onto the authentication object as well.
+ GTMOAuth2SignIn *signIn_;
+
+ // the page request to load when awakeFromNib occurs
+ NSURLRequest *request_;
+
+ // The user we're calling back
+ //
+ // The delegate is retained only until the callback is invoked
+ // or the sign-in is canceled
+ id delegate_;
+ SEL finishedSelector_;
+
+#if NS_BLOCKS_AVAILABLE
+ GTMOAuth2ViewControllerCompletionHandler completionBlock_;
+
+ void (^popViewBlock_)(void);
+#endif
+
+ NSString *keychainItemName_;
+ CFTypeRef keychainItemAccessibility_;
+
+ // if non-nil, the html string to be displayed immediately upon opening
+ // of the web view
+ NSString *initialHTMLString_;
+
+ // set to 1 or -1 if the user sets the showsInitialActivityIndicator
+ // property
+ int mustShowActivityIndicator_;
+
+ // if non-nil, the URL for which cookies will be deleted when the
+ // browser view is dismissed
+ NSURL *browserCookiesURL_;
+
+ id userData_;
+ NSMutableDictionary *properties_;
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
+ // We delegate the decision to our owning NavigationController (if any).
+ // But, the NavigationController will call us back, and ask us.
+ // BOOL keeps us from infinite looping.
+ BOOL isInsideShouldAutorotateToInterfaceOrientation_;
+#endif
+
+ // YES, when view first shown in this signIn session.
+ BOOL isViewShown_;
+
+ // YES, after the view has fully transitioned in.
+ BOOL didViewAppear_;
+
+ // YES between sends of start and stop notifications
+ BOOL hasNotifiedWebViewStartedLoading_;
+
+ // To prevent us from calling our delegate's selector more than once.
+ BOOL hasCalledFinished_;
+
+ // Set in a webView callback.
+ BOOL hasDoneFinalRedirect_;
+
+ // Set during the pop initiated by the sign-in object; otherwise,
+ // viewWillDisappear indicates that some external change of the view
+ // has stopped the sign-in.
+ BOOL didDismissSelf_;
+}
+
+// the application and service name to use for saving the auth tokens
+// to the keychain
+@property (nonatomic, copy) NSString *keychainItemName;
+
+// the keychain item accessibility is a system constant for use
+// with kSecAttrAccessible.
+//
+// Since it's a system constant, we do not need to retain it.
+@property (nonatomic, assign) CFTypeRef keychainItemAccessibility;
+
+// optional html string displayed immediately upon opening the web view
+//
+// This string is visible just until the sign-in web page loads, and
+// may be used for a "Loading..." type of message or to set the
+// initial view color
+@property (nonatomic, copy) NSString *initialHTMLString;
+
+// an activity indicator shows during initial webview load when no initial HTML
+// string is specified, but the activity indicator can be forced to be shown
+// with this property
+@property (nonatomic, assign) BOOL showsInitialActivityIndicator;
+
+// the underlying object to hold authentication tokens and authorize http
+// requests
+@property (nonatomic, retain, readonly) GTMOAuth2Authentication *authentication;
+
+// the underlying object which performs the sign-in networking sequence
+@property (nonatomic, retain, readonly) GTMOAuth2SignIn *signIn;
+
+// user interface elements
+@property (nonatomic, retain) IBOutlet UIButton *backButton;
+@property (nonatomic, retain) IBOutlet UIButton *forwardButton;
+@property (nonatomic, retain) IBOutlet UIActivityIndicatorView *initialActivityIndicator;
+@property (nonatomic, retain) IBOutlet UIView *navButtonsView;
+@property (nonatomic, retain) IBOutlet UIBarButtonItem *rightBarButtonItem;
+@property (nonatomic, retain) IBOutlet UIWebView *webView;
+
+#if NS_BLOCKS_AVAILABLE
+// An optional block to be called when the view should be popped. If not set,
+// the view controller will use its navigation controller to pop the view.
+@property (nonatomic, copy) void (^popViewBlock)(void);
+#endif
+
+// the default timeout for an unreachable network during display of the
+// sign-in page is 30 seconds; set this to 0 to have no timeout
+@property (nonatomic, assign) NSTimeInterval networkLossTimeoutInterval;
+
+// if set, cookies are deleted for this URL when the view is hidden
+//
+// For Google sign-ins, this is set by default to https://google.com/accounts
+// but it may be explicitly set to nil to disable clearing of browser cookies
+@property (nonatomic, retain) NSURL *browserCookiesURL;
+
+// userData is retained for the convenience of the caller
+@property (nonatomic, retain) id userData;
+
+// Stored property values are retained for the convenience of the caller
+- (void)setProperty:(id)obj forKey:(NSString *)key;
+- (id)propertyForKey:(NSString *)key;
+
+@property (nonatomic, retain) NSDictionary *properties;
+
+// Method for creating a controller to authenticate to Google services
+//
+// scope is the requested scope of authorization
+// (like "http://www.google.com/m8/feeds")
+//
+// keychain item name is used for storing the token on the keychain,
+// keychainItemName should be like "My Application: Google Latitude"
+// (or set to nil if no persistent keychain storage is desired)
+//
+// the delegate is retained only until the finished selector is invoked
+// or the sign-in is canceled
+//
+// If you don't like the default nibName and bundle, you can change them
+// using the UIViewController properties once you've made one of these.
+//
+// finishedSelector is called after authentication completes. It should follow
+// this signature.
+//
+// - (void)viewController:(GTMOAuth2ViewControllerTouch *)viewController
+// finishedWithAuth:(GTMOAuth2Authentication *)auth
+// error:(NSError *)error;
+//
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+#if NS_BLOCKS_AVAILABLE
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler;
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler;
+#endif
+#endif
+
+// Create a controller for authenticating to non-Google services, taking
+// explicit endpoint URLs and an authentication object
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName // may be nil
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+// This is the designated initializer
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector;
+
+#if NS_BLOCKS_AVAILABLE
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName // may be nil
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler;
+
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler;
+#endif
+
+// subclasses may override authNibName to specify a custom name
++ (NSString *)authNibName;
+
+// subclasses may override authNibBundle to specify a custom bundle
++ (NSBundle *)authNibBundle;
+
+// subclasses may override setUpNavigation to provide their own navigation
+// controls
+- (void)setUpNavigation;
+
+// apps may replace the sign-in class with their own subclass of it
++ (Class)signInClass;
++ (void)setSignInClass:(Class)theClass;
+
+- (void)cancelSigningIn;
+
+// revocation of an authorized token from Google
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth;
+#endif
+
+//
+// Keychain
+//
+
+// create an authentication object for Google services from the access
+// token and secret stored in the keychain; if no token is available, return
+// an unauthorized auth object. OK to pass NULL for the error parameter.
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ error:(NSError **)error;
+// Equivalent to calling the method above with a NULL error parameter.
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret;
+#endif
+
+// add tokens from the keychain, if available, to the authentication object
+//
+// returns YES if the authentication object was authorized from the keychain
++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth
+ error:(NSError **)error;
+
+// method for deleting the stored access token and secret, useful for "signing
+// out"
++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName;
+
+// method for saving the stored access token and secret
+//
+// returns YES if the save was successful. OK to pass NULL for the error
+// parameter.
++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
+ accessibility:(CFTypeRef)accessibility
+ authentication:(GTMOAuth2Authentication *)auth
+ error:(NSError **)error;
+
+// older version, defaults to kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly
++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth;
+
+@end
+
+// To function, GTMOAuth2ViewControllerTouch needs a certain amount of access
+// to the iPhone's keychain. To keep things simple, its keychain access is
+// broken out into a helper class. We declare it here in case you'd like to use
+// it too, to store passwords.
+
+enum {
+ kGTMOAuth2KeychainErrorBadArguments = -1301,
+ kGTMOAuth2KeychainErrorNoPassword = -1302
+};
+
+
+@interface GTMOAuth2Keychain : NSObject
+
++ (GTMOAuth2Keychain *)defaultKeychain;
+
+// OK to pass nil for the error parameter.
+- (NSString *)passwordForService:(NSString *)service
+ account:(NSString *)account
+ error:(NSError **)error;
+
+// OK to pass nil for the error parameter.
+- (BOOL)removePasswordForService:(NSString *)service
+ account:(NSString *)account
+ error:(NSError **)error;
+
+// OK to pass nil for the error parameter.
+//
+// accessibility should be one of the constants for kSecAttrAccessible
+// such as kSecAttrAccessibleWhenUnlocked
+- (BOOL)setPassword:(NSString *)password
+ forService:(NSString *)service
+ accessibility:(CFTypeRef)accessibility
+ account:(NSString *)account
+ error:(NSError **)error;
+
+// For unit tests: allow setting a mock object
++ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain;
+
+@end
+
+#endif // TARGET_OS_IPHONE
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m
new file mode 100644
index 00000000..3a18104d
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewControllerTouch.m
@@ -0,0 +1,1070 @@
+/* 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.
+ */
+
+//
+// GTMOAuth2ViewControllerTouch.m
+//
+
+#import <Foundation/Foundation.h>
+#import <Security/Security.h>
+
+#if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
+
+#if TARGET_OS_IPHONE
+
+#define GTMOAUTH2VIEWCONTROLLERTOUCH_DEFINE_GLOBALS 1
+#import "GTMOAuth2ViewControllerTouch.h"
+
+#import "GTMOAuth2SignIn.h"
+#import "GTMOAuth2Authentication.h"
+
+static NSString * const kGTMOAuth2AccountName = @"OAuth";
+static GTMOAuth2Keychain* sDefaultKeychain = nil;
+
+@interface GTMOAuth2ViewControllerTouch()
+
+@property (nonatomic, copy) NSURLRequest *request;
+
+- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request;
+- (void)signIn:(GTMOAuth2SignIn *)signIn
+finishedWithAuth:(GTMOAuth2Authentication *)auth
+ error:(NSError *)error;
+- (BOOL)isNavigationBarTranslucent;
+- (void)moveWebViewFromUnderNavigationBar;
+- (void)popView;
+- (void)clearBrowserCookies;
+@end
+
+@implementation GTMOAuth2ViewControllerTouch
+
+// IBOutlets
+@synthesize request = request_,
+ backButton = backButton_,
+ forwardButton = forwardButton_,
+ navButtonsView = navButtonsView_,
+ rightBarButtonItem = rightBarButtonItem_,
+ webView = webView_,
+ initialActivityIndicator = initialActivityIndicator_;
+
+@synthesize keychainItemName = keychainItemName_,
+ keychainItemAccessibility = keychainItemAccessibility_,
+ initialHTMLString = initialHTMLString_,
+ browserCookiesURL = browserCookiesURL_,
+ signIn = signIn_,
+ userData = userData_,
+ properties = properties_;
+
+#if NS_BLOCKS_AVAILABLE
+@synthesize popViewBlock = popViewBlock_;
+#endif
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+ return [[[self alloc] initWithScope:scope
+ clientID:clientID
+ clientSecret:clientSecret
+ keychainItemName:keychainItemName
+ delegate:delegate
+ finishedSelector:finishedSelector] autorelease];
+}
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+ // convenient entry point for Google authentication
+
+ Class signInClass = [[self class] signInClass];
+
+ GTMOAuth2Authentication *auth;
+ auth = [signInClass standardGoogleAuthenticationForScope:scope
+ clientID:clientID
+ clientSecret:clientSecret];
+ NSURL *authorizationURL = [signInClass googleAuthorizationURL];
+ return [self initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ delegate:delegate
+ finishedSelector:finishedSelector];
+}
+
+#if NS_BLOCKS_AVAILABLE
+
++ (id)controllerWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
+ return [[[self alloc] initWithScope:scope
+ clientID:clientID
+ clientSecret:clientSecret
+ keychainItemName:keychainItemName
+ completionHandler:handler] autorelease];
+}
+
+- (id)initWithScope:(NSString *)scope
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
+ // convenient entry point for Google authentication
+
+ Class signInClass = [[self class] signInClass];
+
+ GTMOAuth2Authentication *auth;
+ auth = [signInClass standardGoogleAuthenticationForScope:scope
+ clientID:clientID
+ clientSecret:clientSecret];
+ NSURL *authorizationURL = [signInClass googleAuthorizationURL];
+ self = [self initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ delegate:nil
+ finishedSelector:NULL];
+ if (self) {
+ completionBlock_ = [handler copy];
+ }
+ return self;
+}
+
+#endif // NS_BLOCKS_AVAILABLE
+#endif // !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
+
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+ return [[[self alloc] initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ delegate:delegate
+ finishedSelector:finishedSelector] autorelease];
+}
+
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ delegate:(id)delegate
+ finishedSelector:(SEL)finishedSelector {
+
+ NSString *nibName = [[self class] authNibName];
+ NSBundle *nibBundle = [[self class] authNibBundle];
+
+ self = [super initWithNibName:nibName bundle:nibBundle];
+ if (self != nil) {
+ delegate_ = [delegate retain];
+ finishedSelector_ = finishedSelector;
+
+ Class signInClass = [[self class] signInClass];
+
+ // use the supplied auth and OAuth endpoint URLs
+ signIn_ = [[signInClass alloc] initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ delegate:self
+ webRequestSelector:@selector(signIn:displayRequest:)
+ finishedSelector:@selector(signIn:finishedWithAuth:error:)];
+
+ // if the user is signing in to a Google service, we'll delete the
+ // Google authentication browser cookies upon completion
+ //
+ // for other service domains, or to disable clearing of the cookies,
+ // set the browserCookiesURL property explicitly
+ NSString *authorizationHost = [signIn_.authorizationURL host];
+ if ([authorizationHost hasSuffix:@".google.com"]) {
+ NSString *urlStr = [NSString stringWithFormat:@"https://%@/",
+ authorizationHost];
+ NSURL *cookiesURL = [NSURL URLWithString:urlStr];
+ [self setBrowserCookiesURL:cookiesURL];
+ }
+
+ [self setKeychainItemName:keychainItemName];
+ }
+ return self;
+}
+
+#if NS_BLOCKS_AVAILABLE
++ (id)controllerWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
+ return [[[self alloc] initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ completionHandler:handler] autorelease];
+}
+
+- (id)initWithAuthentication:(GTMOAuth2Authentication *)auth
+ authorizationURL:(NSURL *)authorizationURL
+ keychainItemName:(NSString *)keychainItemName
+ completionHandler:(GTMOAuth2ViewControllerCompletionHandler)handler {
+ // fall back to the non-blocks init
+ self = [self initWithAuthentication:auth
+ authorizationURL:authorizationURL
+ keychainItemName:keychainItemName
+ delegate:nil
+ finishedSelector:NULL];
+ if (self) {
+ completionBlock_ = [handler copy];
+ }
+ return self;
+}
+#endif
+
+- (void)dealloc {
+ [webView_ setDelegate:nil];
+
+ [backButton_ release];
+ [forwardButton_ release];
+ [initialActivityIndicator_ release];
+ [navButtonsView_ release];
+ [rightBarButtonItem_ release];
+ [webView_ release];
+ [signIn_ release];
+ [request_ release];
+ [delegate_ release];
+#if NS_BLOCKS_AVAILABLE
+ [completionBlock_ release];
+ [popViewBlock_ release];
+#endif
+ [keychainItemName_ release];
+ [initialHTMLString_ release];
+ [browserCookiesURL_ release];
+ [userData_ release];
+ [properties_ release];
+
+ [super dealloc];
+}
+
++ (NSString *)authNibName {
+ // subclasses may override this to specify a custom nib name
+ return @"GTMOAuth2ViewTouch";
+}
+
++ (NSBundle *)authNibBundle {
+ // subclasses may override this to specify a custom nib bundle
+ return nil;
+}
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret {
+ return [self authForGoogleFromKeychainForName:keychainItemName
+ clientID:clientID
+ clientSecret:clientSecret
+ error:NULL];
+}
+
++ (GTMOAuth2Authentication *)authForGoogleFromKeychainForName:(NSString *)keychainItemName
+ clientID:(NSString *)clientID
+ clientSecret:(NSString *)clientSecret
+ error:(NSError **)error {
+ Class signInClass = [self signInClass];
+ NSURL *tokenURL = [signInClass googleTokenURL];
+ NSString *redirectURI = [signInClass nativeClientRedirectURI];
+
+ GTMOAuth2Authentication *auth;
+ auth = [GTMOAuth2Authentication authenticationWithServiceProvider:kGTMOAuth2ServiceProviderGoogle
+ tokenURL:tokenURL
+ redirectURI:redirectURI
+ clientID:clientID
+ clientSecret:clientSecret];
+ [[self class] authorizeFromKeychainForName:keychainItemName
+ authentication:auth
+ error:error];
+ return auth;
+}
+
+#endif
+
++ (BOOL)authorizeFromKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)newAuth
+ error:(NSError **)error {
+ newAuth.accessToken = nil;
+
+ BOOL didGetTokens = NO;
+ GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
+ NSString *password = [keychain passwordForService:keychainItemName
+ account:kGTMOAuth2AccountName
+ error:error];
+ if (password != nil) {
+ [newAuth setKeysForResponseString:password];
+ didGetTokens = YES;
+ }
+ return didGetTokens;
+}
+
++ (BOOL)removeAuthFromKeychainForName:(NSString *)keychainItemName {
+ GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
+ return [keychain removePasswordForService:keychainItemName
+ account:kGTMOAuth2AccountName
+ error:nil];
+}
+
++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
+ authentication:(GTMOAuth2Authentication *)auth {
+ return [self saveParamsToKeychainForName:keychainItemName
+ accessibility:NULL
+ authentication:auth
+ error:NULL];
+}
+
++ (BOOL)saveParamsToKeychainForName:(NSString *)keychainItemName
+ accessibility:(CFTypeRef)accessibility
+ authentication:(GTMOAuth2Authentication *)auth
+ error:(NSError **)error {
+ [self removeAuthFromKeychainForName:keychainItemName];
+ // don't save unless we have a token that can really authorize requests
+ if (![auth canAuthorize]) {
+ if (error) {
+ *error = [NSError errorWithDomain:kGTMOAuth2ErrorDomain
+ code:kGTMOAuth2ErrorTokenUnavailable
+ userInfo:nil];
+ }
+ return NO;
+ }
+
+ if (accessibility == NULL
+ && &kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly != NULL) {
+ accessibility = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
+ }
+
+ // make a response string containing the values we want to save
+ NSString *password = [auth persistenceResponseString];
+ GTMOAuth2Keychain *keychain = [GTMOAuth2Keychain defaultKeychain];
+ return [keychain setPassword:password
+ forService:keychainItemName
+ accessibility:accessibility
+ account:kGTMOAuth2AccountName
+ error:error];
+}
+
+- (void)loadView {
+ NSString *nibPath = nil;
+ NSBundle *nibBundle = [self nibBundle];
+ if (nibBundle == nil) {
+ nibBundle = [NSBundle mainBundle];
+ }
+ NSString *nibName = self.nibName;
+ if (nibName != nil) {
+ nibPath = [nibBundle pathForResource:nibName ofType:@"nib"];
+ }
+ if (nibPath != nil && [[NSFileManager defaultManager] fileExistsAtPath:nibPath]) {
+ [super loadView];
+ } else {
+ // One of the requirements of loadView is that a valid view object is set to
+ // self.view upon completion. Otherwise, subclasses that attempt to
+ // access self.view after calling [super loadView] will enter an infinite
+ // loop due to the fact that UIViewController's -view accessor calls
+ // loadView when self.view is nil.
+ self.view = [[[UIView alloc] init] autorelease];
+
+#if DEBUG
+ NSLog(@"missing %@.nib", nibName);
+#endif
+ }
+}
+
+
+- (void)viewDidLoad {
+ [self setUpNavigation];
+}
+
+- (void)setUpNavigation {
+ rightBarButtonItem_.customView = navButtonsView_;
+ self.navigationItem.rightBarButtonItem = rightBarButtonItem_;
+}
+
+- (void)popView {
+#if NS_BLOCKS_AVAILABLE
+ void (^popViewBlock)() = self.popViewBlock;
+#else
+ id popViewBlock = nil;
+#endif
+
+ if (popViewBlock || self.navigationController.topViewController == self) {
+ if (!self.view.hidden) {
+ // Set the flag to our viewWillDisappear method so it knows
+ // this is a disappearance initiated by the sign-in object,
+ // not the user cancelling via the navigation controller
+ didDismissSelf_ = YES;
+
+ if (popViewBlock) {
+#if NS_BLOCKS_AVAILABLE
+ popViewBlock();
+ self.popViewBlock = nil;
+#endif
+ } else {
+ [self.navigationController popViewControllerAnimated:YES];
+ }
+ self.view.hidden = YES;
+ }
+ }
+}
+
+- (void)notifyWithName:(NSString *)name
+ webView:(UIWebView *)webView
+ kind:(NSString *)kind {
+ BOOL isStarting = [name isEqual:kGTMOAuth2WebViewStartedLoading];
+ if (hasNotifiedWebViewStartedLoading_ == isStarting) {
+ // Duplicate notification
+ //
+ // UIWebView's delegate methods are so unbalanced that there's little
+ // point trying to keep a count, as it could easily end up stuck greater
+ // than zero.
+ //
+ // We don't really have a way to track the starts and stops of
+ // subframe loads, too, as the webView in the notification is always
+ // for the topmost request.
+ return;
+ }
+ hasNotifiedWebViewStartedLoading_ = isStarting;
+
+ // Notification for webview load starting and stopping
+ NSDictionary *dict = [NSDictionary dictionaryWithObjectsAndKeys:
+ webView, kGTMOAuth2WebViewKey,
+ kind, kGTMOAuth2WebViewStopKindKey, // kind may be nil
+ nil];
+ NSNotificationCenter *nc = [NSNotificationCenter defaultCenter];
+ [nc postNotificationName:name
+ object:self
+ userInfo:dict];
+}
+
+- (void)cancelSigningIn {
+ // The application has explicitly asked us to cancel signing in
+ // (so no further callback is required)
+ hasCalledFinished_ = YES;
+
+ [delegate_ autorelease];
+ delegate_ = nil;
+
+#if NS_BLOCKS_AVAILABLE
+ [completionBlock_ autorelease];
+ completionBlock_ = nil;
+#endif
+
+ // The sign-in object's cancel method will close the window
+ [signIn_ cancelSigningIn];
+ hasDoneFinalRedirect_ = YES;
+}
+
+static Class gSignInClass = Nil;
+
++ (Class)signInClass {
+ if (gSignInClass == Nil) {
+ gSignInClass = [GTMOAuth2SignIn class];
+ }
+ return gSignInClass;
+}
+
++ (void)setSignInClass:(Class)theClass {
+ gSignInClass = theClass;
+}
+
+#pragma mark Token Revocation
+
+#if !GTM_OAUTH2_SKIP_GOOGLE_SUPPORT
++ (void)revokeTokenForGoogleAuthentication:(GTMOAuth2Authentication *)auth {
+ [[self signInClass] revokeTokenForGoogleAuthentication:auth];
+}
+#endif
+
+#pragma mark Browser Cookies
+
+- (GTMOAuth2Authentication *)authentication {
+ return self.signIn.authentication;
+}
+
+- (void)clearBrowserCookies {
+ // if browserCookiesURL is non-nil, then get cookies for that URL
+ // and delete them from the common application cookie storage
+ NSURL *cookiesURL = [self browserCookiesURL];
+ if (cookiesURL) {
+ NSHTTPCookieStorage *cookieStorage;
+
+ cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
+ NSArray *cookies = [cookieStorage cookiesForURL:cookiesURL];
+
+ for (NSHTTPCookie *cookie in cookies) {
+ [cookieStorage deleteCookie:cookie];
+ }
+ }
+}
+
+#pragma mark Accessors
+
+- (void)setNetworkLossTimeoutInterval:(NSTimeInterval)val {
+ signIn_.networkLossTimeoutInterval = val;
+}
+
+- (NSTimeInterval)networkLossTimeoutInterval {
+ return signIn_.networkLossTimeoutInterval;
+}
+
+- (BOOL)shouldUseKeychain {
+ NSString *name = self.keychainItemName;
+ return ([name length] > 0);
+}
+
+- (BOOL)showsInitialActivityIndicator {
+ return (mustShowActivityIndicator_ == 1 || initialHTMLString_ == nil);
+}
+
+- (void)setShowsInitialActivityIndicator:(BOOL)flag {
+ mustShowActivityIndicator_ = (flag ? 1 : -1);
+}
+
+#pragma mark User Properties
+
+- (void)setProperty:(id)obj forKey:(NSString *)key {
+ if (obj == nil) {
+ // User passed in nil, so delete the property
+ [properties_ removeObjectForKey:key];
+ } else {
+ // Be sure the property dictionary exists
+ if (properties_ == nil) {
+ [self setProperties:[NSMutableDictionary dictionary]];
+ }
+ [properties_ setObject:obj forKey:key];
+ }
+}
+
+- (id)propertyForKey:(NSString *)key {
+ id obj = [properties_ objectForKey:key];
+
+ // Be sure the returned pointer has the life of the autorelease pool,
+ // in case self is released immediately
+ return [[obj retain] autorelease];
+}
+
+#pragma mark SignIn callbacks
+
+- (void)signIn:(GTMOAuth2SignIn *)signIn displayRequest:(NSURLRequest *)request {
+ // This is the signIn object's webRequest method, telling the controller
+ // to either display the request in the webview, or if the request is nil,
+ // to close the window.
+ //
+ // All web requests and all window closing goes through this routine
+
+#if DEBUG
+ if (self.navigationController) {
+ if (self.navigationController.topViewController != self && request != nil) {
+ NSLog(@"Unexpected: Request to show, when already on top. request %@", [request URL]);
+ } else if(self.navigationController.topViewController != self && request == nil) {
+ NSLog(@"Unexpected: Request to pop, when not on top. request nil");
+ }
+ }
+#endif
+
+ if (request != nil) {
+ const NSTimeInterval kJanuary2011 = 1293840000;
+ BOOL isDateValid = ([[NSDate date] timeIntervalSince1970] > kJanuary2011);
+ if (isDateValid) {
+ // Display the request.
+ self.request = request;
+ // The app may prefer some html other than blank white to be displayed
+ // before the sign-in web page loads.
+ // The first fetch might be slow, so the client programmer may want
+ // to show a local "loading" message.
+ // On iOS 5+, UIWebView will ignore loadHTMLString: if it's followed by
+ // a loadRequest: call, so if there is a "loading" message we defer
+ // the loadRequest: until after after we've drawn the "loading" message.
+ //
+ // If there is no initial html string, we show the activity indicator
+ // unless the user set showsInitialActivityIndicator to NO; if there
+ // is an initial html string, we hide the indicator unless the user set
+ // showsInitialActivityIndicator to YES.
+ NSString *html = self.initialHTMLString;
+ if ([html length] > 0) {
+ [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 1)];
+ [self.webView loadHTMLString:html baseURL:nil];
+ } else {
+ [initialActivityIndicator_ setHidden:(mustShowActivityIndicator_ < 0)];
+ [self.webView loadRequest:request];
+ }
+ } else {
+ // clock date is invalid, so signing in would fail with an unhelpful error
+ // from the server. Warn the user in an html string showing a watch icon,
+ // question mark, and the system date and time. Hopefully this will clue
+ // in brighter users, or at least give them a clue when they report the
+ // problem to developers.
+ //
+ // Even better is for apps to check the system clock and show some more
+ // helpful, localized instructions for users; this is really a fallback.
+ NSString *const html = @"<html><body><div align=center><font size='7'>"
+ @"&#x231A; ?<br><i>System Clock Incorrect</i><br>%@"
+ @"</font></div></body></html>";
+ NSString *errHTML = [NSString stringWithFormat:html, [NSDate date]];
+
+ [[self webView] loadHTMLString:errHTML baseURL:nil];
+ }
+ } else {
+ // request was nil.
+ [self popView];
+ }
+}
+
+- (void)signIn:(GTMOAuth2SignIn *)signIn
+ finishedWithAuth:(GTMOAuth2Authentication *)auth
+ error:(NSError *)error {
+ if (!hasCalledFinished_) {
+ hasCalledFinished_ = YES;
+
+ if (error == nil) {
+ if (self.shouldUseKeychain) {
+ NSString *keychainItemName = self.keychainItemName;
+ if (auth.canAuthorize) {
+ // save the auth params in the keychain
+ CFTypeRef accessibility = self.keychainItemAccessibility;
+ [[self class] saveParamsToKeychainForName:keychainItemName
+ accessibility:accessibility
+ authentication:auth
+ error:NULL];
+ } else {
+ // remove the auth params from the keychain
+ [[self class] removeAuthFromKeychainForName:keychainItemName];
+ }
+ }
+ }
+
+ if (delegate_ && finishedSelector_) {
+ SEL sel = finishedSelector_;
+ NSMethodSignature *sig = [delegate_ methodSignatureForSelector:sel];
+ NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
+ [invocation setSelector:sel];
+ [invocation setTarget:delegate_];
+ [invocation setArgument:&self atIndex:2];
+ [invocation setArgument:&auth atIndex:3];
+ [invocation setArgument:&error atIndex:4];
+ [invocation invoke];
+ }
+
+ [delegate_ autorelease];
+ delegate_ = nil;
+
+#if NS_BLOCKS_AVAILABLE
+ if (completionBlock_) {
+ completionBlock_(self, auth, error);
+
+ // release the block here to avoid a retain loop on the controller
+ [completionBlock_ autorelease];
+ completionBlock_ = nil;
+ }
+#endif
+ }
+}
+
+- (void)moveWebViewFromUnderNavigationBar {
+ CGRect dontCare;
+ CGRect webFrame = self.view.bounds;
+ UINavigationBar *navigationBar = self.navigationController.navigationBar;
+ CGRectDivide(webFrame, &dontCare, &webFrame,
+ navigationBar.frame.size.height, CGRectMinYEdge);
+ [self.webView setFrame:webFrame];
+}
+
+// isTranslucent is defined in iPhoneOS 3.0 on.
+- (BOOL)isNavigationBarTranslucent {
+ UINavigationBar *navigationBar = [[self navigationController] navigationBar];
+ BOOL isTranslucent =
+ ([navigationBar respondsToSelector:@selector(isTranslucent)] &&
+ [navigationBar isTranslucent]);
+ return isTranslucent;
+}
+
+#pragma mark -
+#pragma mark Protocol implementations
+
+- (void)viewWillAppear:(BOOL)animated {
+ // See the comment on clearBrowserCookies in viewDidDisappear.
+ [self clearBrowserCookies];
+
+ if (!isViewShown_) {
+ isViewShown_ = YES;
+ if ([self isNavigationBarTranslucent]) {
+ [self moveWebViewFromUnderNavigationBar];
+ }
+ if (![signIn_ startSigningIn]) {
+ // Can't start signing in. We must pop our view.
+ // UIWebview needs time to stabilize. Animations need time to complete.
+ // We remove ourself from the view stack after that.
+ [self performSelector:@selector(popView)
+ withObject:nil
+ afterDelay:0.5
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
+ }
+ }
+ [super viewWillAppear:animated];
+}
+
+- (void)viewDidAppear:(BOOL)animated {
+ didViewAppear_ = YES;
+ [super viewDidAppear:animated];
+}
+
+- (void)viewWillDisappear:(BOOL)animated {
+ if (!didDismissSelf_) {
+ // We won't receive further webview delegate messages, so be sure the
+ // started loading notification is balanced, if necessary
+ [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
+ webView:self.webView
+ kind:kGTMOAuth2WebViewCancelled];
+
+ // We are not popping ourselves, so presumably we are being popped by the
+ // navigation controller; tell the sign-in object to close up shop
+ //
+ // this will indirectly call our signIn:finishedWithAuth:error: method
+ // for us
+ [signIn_ windowWasClosed];
+
+#if NS_BLOCKS_AVAILABLE
+ self.popViewBlock = nil;
+#endif
+ }
+
+ [super viewWillDisappear:animated];
+}
+
+- (void)viewDidDisappear:(BOOL)animated {
+ [super viewDidDisappear:animated];
+
+ // prevent the next sign-in from showing in the WebView that the user is
+ // already signed in. It's possible for the WebView to set the cookies even
+ // after this, so we also clear them when the view first appears.
+ [self clearBrowserCookies];
+}
+
+- (void)viewDidLayoutSubviews {
+ // We don't call super's version of this method because
+ // -[UIViewController viewDidLayoutSubviews] is documented as a no-op, that
+ // didn't exist before iOS 5.
+ [initialActivityIndicator_ setCenter:[webView_ center]];
+}
+
+- (BOOL)webView:(UIWebView *)webView
+ shouldStartLoadWithRequest:(NSURLRequest *)request
+ navigationType:(UIWebViewNavigationType)navigationType {
+
+ if (!hasDoneFinalRedirect_) {
+ hasDoneFinalRedirect_ = [signIn_ requestRedirectedToRequest:request];
+ if (hasDoneFinalRedirect_) {
+ // signIn has told the view to close
+ return NO;
+ }
+ }
+ return YES;
+}
+
+- (void)updateUI {
+ [backButton_ setEnabled:[[self webView] canGoBack]];
+ [forwardButton_ setEnabled:[[self webView] canGoForward]];
+}
+
+- (void)webViewDidStartLoad:(UIWebView *)webView {
+ [self notifyWithName:kGTMOAuth2WebViewStartedLoading
+ webView:webView
+ kind:nil];
+ [self updateUI];
+}
+
+- (void)webViewDidFinishLoad:(UIWebView *)webView {
+ [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
+ webView:webView
+ kind:kGTMOAuth2WebViewFinished];
+
+ NSString *title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
+ if ([title length] > 0) {
+ [signIn_ titleChanged:title];
+ } else {
+#if DEBUG
+ // Verify that Javascript is enabled
+ NSString *result = [webView stringByEvaluatingJavaScriptFromString:@"1+1"];
+ NSAssert([result integerValue] == 2, @"GTMOAuth2: Javascript is required");
+#endif
+ }
+
+ if (self.request && [self.initialHTMLString length] > 0) {
+ // The request was pending.
+ [self setInitialHTMLString:nil];
+ [self.webView loadRequest:self.request];
+ } else {
+ [initialActivityIndicator_ setHidden:YES];
+ [signIn_ cookiesChanged:[NSHTTPCookieStorage sharedHTTPCookieStorage]];
+
+ [self updateUI];
+ }
+}
+
+- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
+ [self notifyWithName:kGTMOAuth2WebViewStoppedLoading
+ webView:webView
+ kind:kGTMOAuth2WebViewFailed];
+
+ // Tell the sign-in object that a load failed; if it was the authorization
+ // URL, it will pop the view and return an error to the delegate.
+ if (didViewAppear_) {
+ BOOL isUserInterruption = ([error code] == NSURLErrorCancelled
+ && [[error domain] isEqual:NSURLErrorDomain]);
+ if (isUserInterruption) {
+ // Ignore this error:
+ // Users report that this error occurs when clicking too quickly on the
+ // accept button, before the page has completely loaded. Ignoring
+ // this error seems to provide a better experience than does immediately
+ // cancelling sign-in.
+ //
+ // This error also occurs whenever UIWebView is sent the stopLoading
+ // message, so if we ever send that message intentionally, we need to
+ // revisit this bypass.
+ return;
+ }
+
+ [signIn_ loadFailedWithError:error];
+ } else {
+ // UIWebview needs time to stabilize. Animations need time to complete.
+ [signIn_ performSelector:@selector(loadFailedWithError:)
+ withObject:error
+ afterDelay:0.5
+ inModes:[NSArray arrayWithObject:NSRunLoopCommonModes]];
+ }
+}
+
+#if __IPHONE_OS_VERSION_MIN_REQUIRED < 60000
+// When running on a device with an OS version < 6, this gets called.
+//
+// Since it is never called in iOS 6 or greater, if your min deployment
+// target is iOS6 or greater, then you don't need to have this method compiled
+// into your app.
+//
+// When running on a device with an OS version 6 or greater, this code is
+// not called. - (NSUInteger)supportedInterfaceOrientations; would be called,
+// if it existed. Since it is absent,
+// Allow the default orientations: All for iPad, all but upside down for iPhone.
+- (BOOL)shouldAutorotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation {
+ BOOL value = YES;
+ if (!isInsideShouldAutorotateToInterfaceOrientation_) {
+ isInsideShouldAutorotateToInterfaceOrientation_ = YES;
+ UIViewController *navigationController = [self navigationController];
+ if (navigationController != nil) {
+ value = [navigationController shouldAutorotateToInterfaceOrientation:interfaceOrientation];
+ } else {
+ value = [super shouldAutorotateToInterfaceOrientation:interfaceOrientation];
+ }
+ isInsideShouldAutorotateToInterfaceOrientation_ = NO;
+ }
+ return value;
+}
+#endif
+
+
+@end
+
+
+#pragma mark Common Code
+
+@implementation GTMOAuth2Keychain
+
++ (GTMOAuth2Keychain *)defaultKeychain {
+ if (sDefaultKeychain == nil) {
+ sDefaultKeychain = [[self alloc] init];
+ }
+ return sDefaultKeychain;
+}
+
+
+// For unit tests: allow setting a mock object
++ (void)setDefaultKeychain:(GTMOAuth2Keychain *)keychain {
+ if (sDefaultKeychain != keychain) {
+ [sDefaultKeychain release];
+ sDefaultKeychain = [keychain retain];
+ }
+}
+
+- (NSString *)keyForService:(NSString *)service account:(NSString *)account {
+ return [NSString stringWithFormat:@"com.google.GTMOAuth.%@%@", service, account];
+}
+
+// The Keychain API isn't available on the iPhone simulator in SDKs before 3.0,
+// so, on early simulators we use a fake API, that just writes, unencrypted, to
+// NSUserDefaults.
+#if TARGET_IPHONE_SIMULATOR && __IPHONE_OS_VERSION_MAX_ALLOWED < 30000
+#pragma mark Simulator
+
+// Simulator - just simulated, not secure.
+- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
+ NSString *result = nil;
+ if (0 < [service length] && 0 < [account length]) {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSString *key = [self keyForService:service account:account];
+ result = [defaults stringForKey:key];
+ if (result == nil && error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:kGTMOAuth2KeychainErrorNoPassword
+ userInfo:nil];
+ }
+ } else if (error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:kGTMOAuth2KeychainErrorBadArguments
+ userInfo:nil];
+ }
+ return result;
+
+}
+
+
+// Simulator - just simulated, not secure.
+- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
+ BOOL didSucceed = NO;
+ if (0 < [service length] && 0 < [account length]) {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSString *key = [self keyForService:service account:account];
+ [defaults removeObjectForKey:key];
+ [defaults synchronize];
+ } else if (error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:kGTMOAuth2KeychainErrorBadArguments
+ userInfo:nil];
+ }
+ return didSucceed;
+}
+
+// Simulator - just simulated, not secure.
+- (BOOL)setPassword:(NSString *)password
+ forService:(NSString *)service
+ accessibility:(CFTypeRef)accessibility
+ account:(NSString *)account
+ error:(NSError **)error {
+ BOOL didSucceed = NO;
+ if (0 < [password length] && 0 < [service length] && 0 < [account length]) {
+ NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
+ NSString *key = [self keyForService:service account:account];
+ [defaults setObject:password forKey:key];
+ [defaults synchronize];
+ didSucceed = YES;
+ } else if (error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:kGTMOAuth2KeychainErrorBadArguments
+ userInfo:nil];
+ }
+ return didSucceed;
+}
+
+#else // ! TARGET_IPHONE_SIMULATOR
+#pragma mark Device
+
++ (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
+ NSMutableDictionary *query = [NSMutableDictionary dictionaryWithObjectsAndKeys:
+ (id)kSecClassGenericPassword, (id)kSecClass,
+ @"OAuth", (id)kSecAttrGeneric,
+ account, (id)kSecAttrAccount,
+ service, (id)kSecAttrService,
+ nil];
+ return query;
+}
+
+- (NSMutableDictionary *)keychainQueryForService:(NSString *)service account:(NSString *)account {
+ return [[self class] keychainQueryForService:service account:account];
+}
+
+
+
+// iPhone
+- (NSString *)passwordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
+ OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
+ NSString *result = nil;
+ if (0 < [service length] && 0 < [account length]) {
+ CFDataRef passwordData = NULL;
+ NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
+ [keychainQuery setObject:(id)kCFBooleanTrue forKey:(id)kSecReturnData];
+ [keychainQuery setObject:(id)kSecMatchLimitOne forKey:(id)kSecMatchLimit];
+
+ status = SecItemCopyMatching((CFDictionaryRef)keychainQuery,
+ (CFTypeRef *)&passwordData);
+ if (status == noErr && 0 < [(NSData *)passwordData length]) {
+ result = [[[NSString alloc] initWithData:(NSData *)passwordData
+ encoding:NSUTF8StringEncoding] autorelease];
+ }
+ if (passwordData != NULL) {
+ CFRelease(passwordData);
+ }
+ }
+ if (status != noErr && error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:status
+ userInfo:nil];
+ }
+ return result;
+}
+
+
+// iPhone
+- (BOOL)removePasswordForService:(NSString *)service account:(NSString *)account error:(NSError **)error {
+ OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
+ if (0 < [service length] && 0 < [account length]) {
+ NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
+ status = SecItemDelete((CFDictionaryRef)keychainQuery);
+ }
+ if (status != noErr && error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:status
+ userInfo:nil];
+ }
+ return status == noErr;
+}
+
+// iPhone
+- (BOOL)setPassword:(NSString *)password
+ forService:(NSString *)service
+ accessibility:(CFTypeRef)accessibility
+ account:(NSString *)account
+ error:(NSError **)error {
+ OSStatus status = kGTMOAuth2KeychainErrorBadArguments;
+ if (0 < [service length] && 0 < [account length]) {
+ [self removePasswordForService:service account:account error:nil];
+ if (0 < [password length]) {
+ NSMutableDictionary *keychainQuery = [self keychainQueryForService:service account:account];
+ NSData *passwordData = [password dataUsingEncoding:NSUTF8StringEncoding];
+ [keychainQuery setObject:passwordData forKey:(id)kSecValueData];
+
+ if (accessibility != NULL && &kSecAttrAccessible != NULL) {
+ [keychainQuery setObject:(id)accessibility
+ forKey:(id)kSecAttrAccessible];
+ }
+ status = SecItemAdd((CFDictionaryRef)keychainQuery, NULL);
+ }
+ }
+ if (status != noErr && error != NULL) {
+ *error = [NSError errorWithDomain:kGTMOAuth2KeychainErrorDomain
+ code:status
+ userInfo:nil];
+ }
+ return status == noErr;
+}
+
+#endif // ! TARGET_IPHONE_SIMULATOR
+
+@end
+
+#endif // TARGET_OS_IPHONE
+
+#endif // #if GTM_INCLUDE_OAUTH2 || !GDATA_REQUIRE_SERVICE_INCLUDES
diff --git a/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib
new file mode 100644
index 00000000..4f91fa4a
--- /dev/null
+++ b/example/common/gtm-oauth2/Source/Touch/GTMOAuth2ViewTouch.xib
@@ -0,0 +1,494 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<archive type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="7.10">
+ <data>
+ <int key="IBDocument.SystemTarget">1024</int>
+ <string key="IBDocument.SystemVersion">12C60</string>
+ <string key="IBDocument.InterfaceBuilderVersion">2840</string>
+ <string key="IBDocument.AppKitVersion">1187.34</string>
+ <string key="IBDocument.HIToolboxVersion">625.00</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginVersions">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string key="NS.object.0">1926</string>
+ </object>
+ <object class="NSArray" key="IBDocument.IntegratedClassDependencies">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>IBProxyObject</string>
+ <string>IBUIActivityIndicatorView</string>
+ <string>IBUIBarButtonItem</string>
+ <string>IBUIButton</string>
+ <string>IBUINavigationItem</string>
+ <string>IBUIView</string>
+ <string>IBUIWebView</string>
+ </object>
+ <object class="NSArray" key="IBDocument.PluginDependencies">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.Metadata">
+ <string key="NS.key.0">PluginDependencyRecalculationVersion</string>
+ <integer value="1" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableArray" key="IBDocument.RootObjects" id="1000">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBProxyObject" id="372490531">
+ <string key="IBProxiedObjectIdentifier">IBFilesOwner</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBProxyObject" id="975951072">
+ <string key="IBProxiedObjectIdentifier">IBFirstResponder</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUINavigationItem" id="1047805472">
+ <string key="IBUITitle">OAuth</string>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUIBarButtonItem" id="961671599">
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIStyle">1</int>
+ </object>
+ <object class="IBUIView" id="808907889">
+ <reference key="NSNextResponder"/>
+ <int key="NSvFlags">292</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBUIButton" id="453250804">
+ <reference key="NSNextResponder" ref="808907889"/>
+ <int key="NSvFlags">292</int>
+ <string key="NSFrameSize">{30, 30}</string>
+ <reference key="NSSuperview" ref="808907889"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="981703116"/>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIContentHorizontalAlignment">0</int>
+ <int key="IBUIContentVerticalAlignment">0</int>
+ <string key="IBUITitleShadowOffset">{0, -2}</string>
+ <string key="IBUINormalTitle">◀</string>
+ <object class="NSColor" key="IBUIHighlightedTitleColor" id="193465259">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MQA</bytes>
+ </object>
+ <object class="NSColor" key="IBUIDisabledTitleColor">
+ <int key="NSColorSpace">2</int>
+ <bytes key="NSRGB">MC41OTYwNzg0NiAwLjY4NjI3NDUzIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes>
+ </object>
+ <reference key="IBUINormalTitleColor" ref="193465259"/>
+ <object class="NSColor" key="IBUINormalTitleShadowColor" id="999379443">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MC41AA</bytes>
+ </object>
+ <object class="IBUIFontDescription" key="IBUIFontDescription" id="621440819">
+ <string key="name">Helvetica-Bold</string>
+ <string key="family">Helvetica</string>
+ <int key="traits">2</int>
+ <double key="pointSize">24</double>
+ </object>
+ <object class="NSFont" key="IBUIFont" id="530402572">
+ <string key="NSName">Helvetica-Bold</string>
+ <double key="NSSize">24</double>
+ <int key="NSfFlags">16</int>
+ </object>
+ </object>
+ <object class="IBUIButton" id="981703116">
+ <reference key="NSNextResponder" ref="808907889"/>
+ <int key="NSvFlags">292</int>
+ <string key="NSFrame">{{30, 0}, {30, 30}}</string>
+ <reference key="NSSuperview" ref="808907889"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView"/>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIContentHorizontalAlignment">0</int>
+ <int key="IBUIContentVerticalAlignment">0</int>
+ <string key="IBUITitleShadowOffset">{0, -2}</string>
+ <string key="IBUINormalTitle">▶</string>
+ <reference key="IBUIHighlightedTitleColor" ref="193465259"/>
+ <object class="NSColor" key="IBUIDisabledTitleColor">
+ <int key="NSColorSpace">2</int>
+ <bytes key="NSRGB">MC41ODQzMTM3NSAwLjY3NDUwOTgyIDAuOTUyOTQxMjQgMC42MDAwMDAwMgA</bytes>
+ </object>
+ <reference key="IBUINormalTitleColor" ref="193465259"/>
+ <reference key="IBUINormalTitleShadowColor" ref="999379443"/>
+ <reference key="IBUIFontDescription" ref="621440819"/>
+ <reference key="IBUIFont" ref="530402572"/>
+ </object>
+ </object>
+ <string key="NSFrameSize">{60, 30}</string>
+ <reference key="NSSuperview"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="453250804"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MSAwAA</bytes>
+ </object>
+ <bool key="IBUIOpaque">NO</bool>
+ <bool key="IBUIClearsContextBeforeDrawing">NO</bool>
+ <object class="IBUISimulatedOrientationMetrics" key="IBUISimulatedOrientationMetrics">
+ <int key="IBUIInterfaceOrientation">3</int>
+ <int key="interfaceOrientation">3</int>
+ </object>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ <object class="IBUIView" id="426018584">
+ <reference key="NSNextResponder"/>
+ <int key="NSvFlags">274</int>
+ <object class="NSMutableArray" key="NSSubviews">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBUIWebView" id="663477729">
+ <reference key="NSNextResponder" ref="426018584"/>
+ <int key="NSvFlags">274</int>
+ <string key="NSFrameSize">{320, 460}</string>
+ <reference key="NSSuperview" ref="426018584"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="268967673"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">1</int>
+ <bytes key="NSRGB">MSAxIDEAA</bytes>
+ </object>
+ <bool key="IBUIClipsSubviews">YES</bool>
+ <bool key="IBUIMultipleTouchEnabled">YES</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <int key="IBUIDataDetectorTypes">1</int>
+ <bool key="IBUIDetectsPhoneNumbers">YES</bool>
+ </object>
+ <object class="IBUIActivityIndicatorView" id="268967673">
+ <reference key="NSNextResponder" ref="426018584"/>
+ <int key="NSvFlags">301</int>
+ <string key="NSFrame">{{150, 115}, {20, 20}}</string>
+ <reference key="NSSuperview" ref="426018584"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView"/>
+ <string key="NSReuseIdentifierKey">_NS:9</string>
+ <bool key="IBUIOpaque">NO</bool>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <bool key="IBUIHidesWhenStopped">NO</bool>
+ <bool key="IBUIAnimating">YES</bool>
+ <int key="IBUIStyle">2</int>
+ </object>
+ </object>
+ <string key="NSFrameSize">{320, 460}</string>
+ <reference key="NSSuperview"/>
+ <reference key="NSWindow"/>
+ <reference key="NSNextKeyView" ref="663477729"/>
+ <object class="NSColor" key="IBUIBackgroundColor">
+ <int key="NSColorSpace">3</int>
+ <bytes key="NSWhite">MQA</bytes>
+ <object class="NSColorSpace" key="NSCustomColorSpace">
+ <int key="NSID">2</int>
+ </object>
+ </object>
+ <string key="targetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ </object>
+ </object>
+ <object class="IBObjectContainer" key="IBDocument.Objects">
+ <object class="NSMutableArray" key="connectionRecords">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">rightBarButtonItem</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="961671599"/>
+ </object>
+ <int key="connectionID">20</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">navButtonsView</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="808907889"/>
+ </object>
+ <int key="connectionID">22</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">backButton</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="453250804"/>
+ </object>
+ <int key="connectionID">25</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">forwardButton</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="981703116"/>
+ </object>
+ <int key="connectionID">26</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">view</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="426018584"/>
+ </object>
+ <int key="connectionID">28</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">webView</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="663477729"/>
+ </object>
+ <int key="connectionID">29</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">initialActivityIndicator</string>
+ <reference key="source" ref="372490531"/>
+ <reference key="destination" ref="268967673"/>
+ </object>
+ <int key="connectionID">33</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">delegate</string>
+ <reference key="source" ref="663477729"/>
+ <reference key="destination" ref="372490531"/>
+ </object>
+ <int key="connectionID">9</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchOutletConnection" key="connection">
+ <string key="label">rightBarButtonItem</string>
+ <reference key="source" ref="1047805472"/>
+ <reference key="destination" ref="961671599"/>
+ </object>
+ <int key="connectionID">14</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchEventConnection" key="connection">
+ <string key="label">goBack</string>
+ <reference key="source" ref="453250804"/>
+ <reference key="destination" ref="663477729"/>
+ <int key="IBEventType">7</int>
+ </object>
+ <int key="connectionID">18</int>
+ </object>
+ <object class="IBConnectionRecord">
+ <object class="IBCocoaTouchEventConnection" key="connection">
+ <string key="label">goForward</string>
+ <reference key="source" ref="981703116"/>
+ <reference key="destination" ref="663477729"/>
+ <int key="IBEventType">7</int>
+ </object>
+ <int key="connectionID">19</int>
+ </object>
+ </object>
+ <object class="IBMutableOrderedSet" key="objectRecords">
+ <object class="NSArray" key="orderedObjects">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBObjectRecord">
+ <int key="objectID">0</int>
+ <object class="NSArray" key="object" id="0">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ </object>
+ <reference key="children" ref="1000"/>
+ <nil key="parent"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-1</int>
+ <reference key="object" ref="372490531"/>
+ <reference key="parent" ref="0"/>
+ <string key="objectName">File's Owner</string>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">-2</int>
+ <reference key="object" ref="975951072"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">6</int>
+ <reference key="object" ref="1047805472"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">10</int>
+ <reference key="object" ref="961671599"/>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">15</int>
+ <reference key="object" ref="808907889"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference ref="453250804"/>
+ <reference ref="981703116"/>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">16</int>
+ <reference key="object" ref="453250804"/>
+ <reference key="parent" ref="808907889"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">17</int>
+ <reference key="object" ref="981703116"/>
+ <reference key="parent" ref="808907889"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">27</int>
+ <reference key="object" ref="426018584"/>
+ <object class="NSMutableArray" key="children">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference ref="663477729"/>
+ <reference ref="268967673"/>
+ </object>
+ <reference key="parent" ref="0"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">4</int>
+ <reference key="object" ref="663477729"/>
+ <reference key="parent" ref="426018584"/>
+ </object>
+ <object class="IBObjectRecord">
+ <int key="objectID">31</int>
+ <reference key="object" ref="268967673"/>
+ <reference key="parent" ref="426018584"/>
+ </object>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="flattenedProperties">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>-1.CustomClassName</string>
+ <string>-1.IBPluginDependency</string>
+ <string>-2.CustomClassName</string>
+ <string>-2.IBPluginDependency</string>
+ <string>10.IBPluginDependency</string>
+ <string>15.IBPluginDependency</string>
+ <string>16.IBPluginDependency</string>
+ <string>17.IBPluginDependency</string>
+ <string>27.IBPluginDependency</string>
+ <string>31.IBPluginDependency</string>
+ <string>4.IBPluginDependency</string>
+ <string>6.IBPluginDependency</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>GTMOAuth2ViewControllerTouch</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>UIResponder</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ <string>com.apple.InterfaceBuilder.IBCocoaTouchPlugin</string>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="unlocalizedProperties">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference key="dict.sortedKeys" ref="0"/>
+ <reference key="dict.values" ref="0"/>
+ </object>
+ <nil key="activeLocalization"/>
+ <object class="NSMutableDictionary" key="localizations">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <reference key="dict.sortedKeys" ref="0"/>
+ <reference key="dict.values" ref="0"/>
+ </object>
+ <nil key="sourceID"/>
+ <int key="maxID">33</int>
+ </object>
+ <object class="IBClassDescriber" key="IBDocument.Classes">
+ <object class="NSMutableArray" key="referencedPartialClassDescriptions">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBPartialClassDescription">
+ <string key="className">GTMOAuth2ViewControllerTouch</string>
+ <string key="superclassName">UIViewController</string>
+ <object class="NSMutableDictionary" key="outlets">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>backButton</string>
+ <string>forwardButton</string>
+ <string>initialActivityIndicator</string>
+ <string>navButtonsView</string>
+ <string>rightBarButtonItem</string>
+ <string>webView</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>UIButton</string>
+ <string>UIButton</string>
+ <string>UIActivityIndicatorView</string>
+ <string>UIView</string>
+ <string>UIBarButtonItem</string>
+ <string>UIWebView</string>
+ </object>
+ </object>
+ <object class="NSMutableDictionary" key="toOneOutletInfosByName">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="NSArray" key="dict.sortedKeys">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <string>backButton</string>
+ <string>forwardButton</string>
+ <string>initialActivityIndicator</string>
+ <string>navButtonsView</string>
+ <string>rightBarButtonItem</string>
+ <string>webView</string>
+ </object>
+ <object class="NSArray" key="dict.values">
+ <bool key="EncodedWithXMLCoder">YES</bool>
+ <object class="IBToOneOutletInfo">
+ <string key="name">backButton</string>
+ <string key="candidateClassName">UIButton</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">forwardButton</string>
+ <string key="candidateClassName">UIButton</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">initialActivityIndicator</string>
+ <string key="candidateClassName">UIActivityIndicatorView</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">navButtonsView</string>
+ <string key="candidateClassName">UIView</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">rightBarButtonItem</string>
+ <string key="candidateClassName">UIBarButtonItem</string>
+ </object>
+ <object class="IBToOneOutletInfo">
+ <string key="name">webView</string>
+ <string key="candidateClassName">UIWebView</string>
+ </object>
+ </object>
+ </object>
+ <object class="IBClassDescriptionSource" key="sourceIdentifier">
+ <string key="majorKey">IBProjectSource</string>
+ <string key="minorKey">./Classes/GTMOAuth2ViewControllerTouch.h</string>
+ </object>
+ </object>
+ </object>
+ </object>
+ <int key="IBDocument.localizationMode">0</int>
+ <string key="IBDocument.TargetRuntimeIdentifier">IBCocoaTouchFramework</string>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencies">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
+ <real value="1024" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDependencyDefaults">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.iPhoneOS</string>
+ <real value="1536" key="NS.object.0"/>
+ </object>
+ <object class="NSMutableDictionary" key="IBDocument.PluginDeclaredDevelopmentDependencies">
+ <string key="NS.key.0">com.apple.InterfaceBuilder.CocoaTouchPlugin.InterfaceBuilder3</string>
+ <integer value="3000" key="NS.object.0"/>
+ </object>
+ <bool key="IBDocument.PluginDeclaredDependenciesTrackSystemTargetVersion">YES</bool>
+ <int key="IBDocument.defaultPropertyAccessControl">3</int>
+ <string key="IBCocoaTouchPluginVersion">1926</string>
+ </data>
+</archive>