Labels

tools (7) programming (6) rails (5) cluj (4) productivity (4) misc (3) startups (3) emberjs (2) internet (2) software (2) hack (1) meetup (1) os x (1) science (1)

Saturday, November 26, 2016

Blending OAuth2 authorization with Devise using Ember Simple Auth

My Rails app already had authentication implemented using Devise and was serving some custom session data. Everything was elegantly tied together using Ember Simple Auth. I wanted to allow customers to sign-in / sign-up with Google using the OAuth2 mechanism.
I turned to Torii. Specifically, I was looking into Torii's OAuth2 provider to help me handle these OAuth2 phases:

1. Select & authorize APIs

This requires user consent; the user logs in with their Google account and is asked whether they are willing to grant the permissions that the application is requesting.

// in config/environment.js
var torii = ENV.torii = { providers: {} };
torii.providers['google-oauth2'] = {
      redirectUri: 'http://redirect.uri',
      apiKey:      'something.apps.googleusercontent.com',
      scope:       'email profile' // NODOC: scopes should be space-separated!
 
} 




2. Exchange authorization code for access tokens

Once the user has granted permission, the Google Authorization Server sends back an access token which allows your application to perform API requests on the user's behalf. Torii could seamlessly handle this exchange, but I wanted to perform the API requests from the backend and leave my client app authenticated with my backend, instead of directly authenticated with the Google API.

When a page loads, Ember Simple Auth looks at the authenticator referenced in the session data (ToriiAuthenticator in my case), and then calls its restore() method. I wanted to hand off the control to Devise, through the Devise authenticator, so I had to short circuit the logic this way:

    authenticateWithGoogle() {
      return this.get('session').authenticate('authenticator:torii', 'google-oauth2').then(() => {
        let authenticated = this.get('session.data.authenticated');
        authenticated.authenticator = 'authenticator:devise';
        getOwner(this).lookup('authenticator:torii').trigger('sessionDataUpdated', authenticated);
      }, (reason) => {
        this.set('errorMessage', reason);
      });
    }

Next, I had to make sure that the right data was being passed to my DeviseAuthenticator, thus I had to write a custom Torii provider, extending the existing GoogleAuth2 provider.

 import GoogleOauth2 from 'torii/providers/google-oauth2';

 export default GoogleOauth2.extend({
   responseParams: ['organization_id', 'user_id', 'token', 'email', 'error'],

   open(options) {
     let url = this.buildUrl(),
       responseParams = this.get('responseParams');

     return this.get('popup').open(url, responseParams, options).then(function(authData) {
       if (authData.error !== '') {
         throw new Error(errorMessage);
       }

      // Some validations for params here

       return {
         organization_id: authData.organization_id,
         user_id:         authData.user_id,
         token:           authData.token,
         email:           decodeURIComponent(authData.email)
       };
     });
   }
 });

And just a little extra on the Rails controller handling OAuth authentication:

class Users::OauthController < ApplicationController
  GOOGLE_PROVIDER = 'google'.freeze

  def google
    user = User.find_by_identity_provider(oauth_userinfo.id, GOOGLE_PROVIDER)

    if user.present?
      redirect_to root_path(return_oauth_session(user))
    else
      new_user = build_user_from_oauth
      redirect_to root_path(return_oauth_session(new_user)
    end
  end
end

And there you have it, Ember Simple Auth, together with the abstractions which Torii provides, make a powerful and highly versatile toolset for building a complete auth* solution!

No comments:

Post a Comment