Friday, May 15, 2015

Manually Getting OAuth Tokens in Meteor

Meteor is a great framework that provides several features/packages that accelerate application development. One very useful feature is Meteor Accounts. With just a few lines of code you can add a full featured user account system that supports login, logout, account creation and support for OAuth providers like Google and Facebook. If you just want to get things up and running there's no need to understand the details of the OAuth mechanism as Meteor handles that for you automagically. However, there may be times when you don't want to or can't use the Meteor Accounts package to do OAuth. Perhaps, the application needs to access an account that is different from the logged in user, like a shared support email account. In this case, you need to handle the OAuth manually. Fortunately, handling OAuth with Meteor is fairly straightforward. Let's look at what that entails.

We'll look at the process for Google OAuth but the same concept should apply to other providers. The basic steps are:
  • Create client credentials (client_id, client_secret) in the control panel for the OAuth provider. This will including setting up the redirect_uri 
  • Generate the login Url using the client_id, client_secret, and redirect_uri
  • Implement the redirect_uri in your app to capture the auth code 
  • Exchange the auth code for an access token (and refresh token) 
  •  If the access token expires, get a new access token using the refresh token
Create Client Credentials
In the Google Developer Console, go to the project then APIs & auth > Credentials. Click to "Create new Client ID" and you will see something like:
screen show of Create Client ID
For the authorized JavaScript origins and Authorized redirect URIs you can enter: http://localhost:3000 and http://localhost:3000/oauth.
Generate the login URL
In your login template, create a helper to generate the login URL using the client credentials. For example:
Template.login.helpers({

    loginUrl: function() { 
        var loginUrl = "https://accounts.google.com/o/oauth2/auth?scope=" + Meteor.settings.public.scope
            + "&redirect_uri=" + Meteor.settings.public.redirect_uri
            + "&response_type=code&client_id=" + Meteor.settings.public.client_id
            + "&access_type=offline";
        return loginUrl;
    }
});

Implement the redirect_uri

If the redirect_uri is set to http://localhost:3000/oauth, then set up a route to this url. Using iron:router:

Router.route('/oauth',  {name: 'oauth'} );

In the "oauth" template, we'll set up a button to get the authentication code from the url and call a Meteor method to exchange the code for an access token. In oauth.js we'll implement a function to handle the click:
Template.oauth.events({
    'click #getTokensButton': function(){        
        var controller = Iron.controller();
        var code = controller.getParams().query.code;
        Meteor.call("exchangeCodeForTokens", code, function(error, results) {
            console.log(results); 
        });
    }
});

Here is the Meteor method:
Meteor.methods({
    
    exchangeCodeForTokens: function (code) {
        this.unblock();
        var apiUrl = "https://www.googleapis.com/oauth2/v3/token";
        try 
        {        
            var result = HTTP.post( apiUrl,
                {
                params: {                                        
                    'code': code,
                    'client_id': Meteor.settings.public.client_id,
                    'client_secret': Meteor.settings.client_secret,
                    'redirect_uri': Meteor.settings.public.redirect_uri,
                    'grant_type': 'authorization_code'
                }
            });            
            Tokens.upsert(
                {account: Meteor.settings.account},
                { $set:
                    {                        
                        accessToken: result.data.access_token,
                        expiresIn: result.data.expires_in
                        
                    }
                }
            );
            if (result.data.refresh_token) {
                Tokens.upsert(
                    {account: Meteor.settings.account},
                    { $set:
                        {                        
                            refreshToken: result.data.refresh_token
                        }
                    }
                );                
            }
            return result;         
        } catch(error){
            console.log(error)
            return error;
        }        
    }
});
Make a call to the Google OAuth API with the authorization code. The response will contain the access_token which you can save in a collection. In the login URL above, we specified "access_type=offline" so the response will also include a refresh token. Access tokens expire after a certain amount of time like around 1 hour. Once the token expires you will either have to login in again to get a new authorization code and exchange it for a new token or if you obtained a refresh token you can exchange the refresh token for a new access token. The Meteor method to exchange the refresh token looks like:
Meteor.methods({
    exchangeRefreshToken: function(){
        console.log("in exchangeRefreshToken...");        
        this.unblock();            
        var tokens = Tokens.findOne();            
        console.log(tokens.refreshToken);
        var apiUrl = "https://www.googleapis.com/oauth2/v3/token";
        var result = HTTP.post( apiUrl, {
            params: {
                'client_id': Meteor.settings.public.client_id,
                'client_secret': Meteor.settings.client_secret,
                'refresh_token': tokens.refreshToken,
                'grant_type': 'refresh_token'
            }
        });
        
        if (result.data.access_token){
            Tokens.upsert(
                {account: Meteor.settings.account},
                    { $set:
                        {                        
                            accessToken: result.data.access_token,
                            expiresIn: result.data.expires_in
                        }
                    }                
            );            
        }
        return result.data;
    }

});
That's all there is to it! If  you want to see an example app that implements this, check out https://github.com/philcruz/meteor-gmail-example.

More information:
Google - Using OAuth 2.0 for Web Server Applications