Monday, June 2, 2014

OAuth2 Refresh

We've been talking in length about authorization code grant in my last previous posts.

If we go back to you GoogleDrive exemple (remember OAuth2 discussion part2) let's see how we can implement refresh grant. From OAuth2 spec, if you request an authorization code grant, you should have received a refresh token at the same time you got your access token. Access token expired in 1 hour (for Google for ex) and if you don't want to prompt again for granting access your end-users, you need to use refresh token.

What are they for?

Quoting from ietf mailing list:
There is a security reason, the refresh_token is only ever exchanged with authorization server whereas the access_token is exchanged with resource servers. This mitigates the risk of a long-lived access_token leaking (query param in a log file on an insecure resource server, beta or poorly coded resource server app, JS SDK client on a non https site that puts the access_token in a cookie, etc) in the "an access token good for an hour, with a refresh token good for a year or good-till-revoked" vs "an access token good-till-revoked without a refresh token." Long live tokens are seeing as a security issue whereas short live token + refresh token mitigates the risk.

How to ask?

Refreshing an access token is trivial, it's just a matter of sending you refresh token with an grant_type set to refresh_token.

Not always available...

Facebook for instance don't go the OAuth2 way. After dropping support for long live access token in offline mode, Facebook goes the path of exchanging short lived access token for long lived user access token (60 days). Not quite a refresh_token. The long lived_token does expired too whereas the refresh token lives until revoked.

Going back to code

Remember GoogleDrive client app which main goal is to connect to google drive and retrieve a list of document. Let's see how AeroGear OAuth2 deal with refreshing tokens.

In the main ViewController file, AGViewController.m:
    AGAuthorizer* authorizer = [AGAuthorizer authorizer];
    
    _restAuthzModule = [authorizer authz:^(id config) {      [1]
        config.name = @"restAuthMod";
        config.baseURL = [[NSURL alloc] initWithString:@"https://accounts.google.com"];
        config.authzEndpoint = @"/o/oauth2/auth";
        config.accessTokenEndpoint = @"/o/oauth2/token";
        config.revokeTokenEndpoint = @"/o/oauth2/revoke";
        config.clientId = @"XXX";
        config.redirectURL = @"org.aerogear.GoogleDrive:/oauth2Callback";
        config.scopes = @[@"https://www.googleapis.com/auth/drive"];
    }];  

    NSString* readGoogleDriveURL = @"https://www.googleapis.com/drive/v2";
    NSURL* serverURL = [NSURL URLWithString:readGoogleDriveURL];
    AGPipeline* googleDocuments = [AGPipeline pipelineWithBaseURL:serverURL];

    id documents = [googleDocuments pipe:^(id config) {       [2]
        [config setName:@"files"];
        [config setAuthzModule:authzModule];                                        [3]
    }];

    [documents read:^(id responseObject) {                                          [4]
        _documents = [[self buildDocumentList:responseObject[0]] copy];
        [self.tableView reloadData];
    } failure:^(NSError *error) {
        // when an error occurs... at least log it to the console..
        NSLog(@"Read: An error occurred! \n%@", error);
    }];


In [1], Create an Authorization module, in [2] create a Pipe to read your Google Drive documents, injecting the authzModule into the pipe, and then we simply read the pipe!
You don't have to explicitly call requestAccessSuccess:failure: before reading a Pipe associated to an authzModule. If you don't call it the request will be done on your first CRUD operation on the pipe. However if you prefer to control when you want to ask the end-user for grant permission, you can call it explicitly.

Behind the hood, what is AeroGear doing for us?

Inside AeroGear iOS code

When you call a Pipe.read, AeroGear implicitly wrapped this call within a requestAccessSuccess:failure: call. But what does this method do? well it depends on you state...
-(void) requestAccessSuccess:(void (^)(id object))success
                     failure:(void (^)(NSError *error))failure {
    if (self.session.accessToken != nil && [self.session tokenIsNotExpired]) {
        // we already have a valid access token, nothing more to be done
        if (success) {
            success(self.session.accessToken);
        }
    } else if (self.session.refreshToken != nil) {
        // need to refresh token
        [self refreshAccessTokenSuccess:success failure:failure];
    } else {
        // ask for authorization code and once obtained exchange code for access token
        [self requestAuthorizationCodeSuccess:success failure:failure];
    }
}
If you already have valid token, you're fine just forward success, if your access token has expired but you have a refresh token, just go for a refresh and last if you don't have any them, you need to go for the full grant pop-up.

Don't want to ask grant at each start-up

Ok fine, I have a mechanism to be able to ask only once at each client start up for access token and refresh token, then if I don't want to ask each time I start the app. Storing the tokens seem the way to go... I'll tell you more about AGAccountManager in my next blog post and how you can safely store your tokens.

To see complete source code for GoogleDrive app go to aerogear-ios-cookbook.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.