diff options
Diffstat (limited to 'example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m')
-rw-r--r-- | example/common/gtm-oauth2/Source/GTMOAuth2Authentication.m | 1275 |
1 files changed, 1275 insertions, 0 deletions
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 |