Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use a Google offline access token to authenticate in background on iOS?

Short version:

I want to use the Google OAuth2 client for Objective C to create a valid GTMOAuth2Authentication object for me to use in my app, with an offline access token I get from my backend. How can I accomplish that?

My situation:

My app uses Analytics read-only scope to get some data about the user's website(s). The way I accomplish this, is by logging in with the GTMOAuth2ViewControllerTouch ViewController, provided by the Google OAuth2 client for Objective C. It then gives me the valid GTMOAuth2Authentication object, that I can use to query Google Analytics via the Google API client.

Now, we don't want to give the users Google Analytics access (some don't have Google accounts and in general, we want to keep the information simple via the app). This means we got to login with our account (that has access to all the websites' Analytics data). Obviously we can't give our users our credentials, so we had to find a solution for that.

My plan:

I think this problem could be solved by requesting our (offline access) token string from our backend (via SSL encryption), saving it to the user's keychain and use it further in the application to query Analytics. We then let the user login at our service (so we can determine which websites the user has access to), and show the data.

My problem:

I have searched everywhere, looked through the (very thin) documentation by Google, inspected the GTMOAuth2Authentication source code, but I cannot seem to wrap my head around the problem. It seems to me there would be a solution like this (because I use a similar approach in our CMS to let a user post to our Facebook wall), so what am I missing here?

The current login code:

GTMOAuth2ViewControllerTouch *viewController = [[GTMOAuth2ViewControllerTouch alloc]
                                                    initWithScope:scope
                                                    clientID:kMyClientID
                                                    clientSecret:kMyClientSecret
                                                    keychainItemName:kKeychainItemName
                                                    completionHandler:
    ^(GTMOAuth2ViewControllerTouch *viewController, GTMOAuth2Authentication *auth, NSError *error) {
        if (error != nil) {
            // Authentication failed
            DebugLog(@"Failed!");
        } else {
            // Authentication succeeded
            DebugLog(@"Success!");

            // Update interface
            self.loginButton.hidden = YES;

            self.authentication = auth;
        }
    }]; 

What I have tried:

I have tried manually creating a GTMOAuth2Authentication object and setting all the parameters myself (scope, clientid, secretid, access token, refresh token, callbackuri, token url, etc), but I get returned a 401: Login Required error when I try to use the object for queries. So I guess that is not the way to do it.

Links:

  • Google OAuth2 client
  • Google API objective-c client

Thanks for reading!

like image 674
Thermometer Avatar asked Apr 15 '14 19:04

Thermometer


2 Answers

You don't need a GTMOAuth2Authentication instance. What you need is to create you own class that implement the GTMFetcherAuthorizationProtocol protocol, and assign it as your authorizer.

MyAuth *myAuth = [[MyAuth alloc] initWithAccessToken:accessToken];
googleService.authorizer = myAuth;

This class needs to authorize the request based on the access token you already have. The code of this class should be similar to this.

@interface MyAuth : NSObject<GTMFetcherAuthorizationProtocol>     
    @property (copy, nonatomic) NSString *accessToken;
    @property (copy, readonly) NSString *userEmail;
@end

@implementation MyAuth

- (void)authorizeRequest:(NSMutableURLRequest *)request
                delegate:(id)delegate
       didFinishSelector:(SEL)sel {
    if (request) {
        NSString *value = [NSString stringWithFormat:@"%s %@", GTM_OAUTH2_BEARER, self.accessToken];
        [request setValue:value forHTTPHeaderField:@"Authorization"];
    }

    NSMethodSignature *sig = [delegate methodSignatureForSelector:sel];
    NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
    [invocation setSelector:sel];
    [invocation setTarget:delegate];
    [invocation setArgument:(__bridge void *)(self) atIndex:2];
    [invocation setArgument:&request atIndex:3];
    [invocation invoke];
}

- (void)authorizeRequest:(NSMutableURLRequest *)request
       completionHandler:(void (^)(NSError *error))handler {
    if (request) {
        NSString *value = [NSString stringWithFormat:@"%@ %@", @"Bearer", self.accessToken];
        [request setValue:value forHTTPHeaderField:@"Authorization"];
    }
}

- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
    return NO;
}

- (void)stopAuthorization {
}

- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
}

- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
    return YES;
}

Hope it helps.

like image 144
balkoth Avatar answered Oct 13 '22 20:10

balkoth


You need is to create you own class that implement the GTMFetcherAuthorizationProtocol protocol, and assign it as your authorizer.

Here is Xcode 10.1 (2019) updated fixed non-crashing version:

NSString *accessToken = "ya29.c.Elu-Bmq2eNaGPnIe3-Q4GuTAhuQZmyC6ylm6zCROBtyKEJQFJdlIBTUVlUjOOoaT0cQae_OGdxNM4ayRT_0yg121kD8ouX4SGbllPWRkiGHRbsZRuMPX2QCgMoIO";
MyAuth *auth = [[MyAuth alloc] initWithAccessToken:accessToken];
googleService.authorizer = auth;

MyGTMFetcherAuthorization.m

#import <Foundation/Foundation.h>
#import <GTMSessionFetcher/GTMSessionFetcher.h>

@interface MyAuth : NSObject<GTMFetcherAuthorizationProtocol>
+ (MyAuth *)initWithAccessToken:(NSString *)accessToken;
@end

// 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

@interface MyAuth ()
@property (strong, nonatomic) NSString *accessToken;
@end

@implementation MyAuth

+ (MyAuth *)initWithAccessToken:(NSString *)accessToken {
    MyAuth *auth = [[MyAuth alloc] init];
    auth.accessToken = [accessToken copy];
    return auth;
}

- (void)authorizeRequest:(NSMutableURLRequest *)request
                delegate:(id)delegate
       didFinishSelector:(SEL)sel {
    [self setTokeToRequest: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 invoke];
}

- (void)authorizeRequest:(NSMutableURLRequest *)request
   completionHandler:(void (^)(NSError *error))handler {
    [self setTokeToRequest:request];
}

- (void)setTokeToRequest:(NSMutableURLRequest *)request {
    if (request) {
        NSString *value = [NSString stringWithFormat:@"%s %@", GTM_OAUTH2_BEARER, self.accessToken];
        [request setValue:value forHTTPHeaderField:@"Authorization"];
    }
}

- (BOOL)isAuthorizedRequest:(NSURLRequest *)request {
    return NO;
}

- (void)stopAuthorization {
}

- (void)stopAuthorizationForRequest:(NSURLRequest *)request {
}

- (BOOL)isAuthorizingRequest:(NSURLRequest *)request {
    return YES;
}
@end
like image 37
Argus Avatar answered Oct 13 '22 21:10

Argus