OAuth Implicit Grant with Grape, Doorkeeper and AngularJS

Adam Hodowany

OAuth Implicit Grant with Grape, Doorkeeper and AngularJS

Some time ago we implemented authentication with OAuth Implicit Flow using:

and, of course, AngularJS and Ruby on Rails. We thought that this case was interesting enough to write about.

If you're not sure how OAuth Implicit Flow works, it would be good idea to read this article first: OAuth2: the Implicit Flow, aka as the Client-Side Flow . It's a flow for clients that can't keep secrets - exactly what we need for SPAs.

This solution redirects the user from our front-end application (AngularJS) to the back-end application (Rails) with a login form (Devise). The user is then redirected back to the front-end application with an Access Token that we'll attach to every request (Grape) in order to authenticate the user (Doorkeeper). This is our happy path:

Diagram

Demo

We've prepared a sample demo application with meaningful commits, so you can run it and play with it:

DEMO

Setup

I won't focus too much on the setup, but here's what we need to start with:

  • Rails configured with AngularJS,
  • Devise for User model (database_authenticable and validatable modules are enough) with login form,
  • Grape for users that's ready to respond to /,
  • AngularJS with ui-router.

You can git reset our demo application to cfa3b69 to get this post-setup state and work on implementing the fun stuff with us as we go along.

Now, let's get to the good stuff.

Rails

Gemfile

# Gemfile
gem 'doorkeeper',       '~> 1.4.0'
gem 'grape-doorkeeper', '~> 0.0.2'

(114d855)

grape-doorkeeper is a fine little gem that integrates Grape with Doorkeeper almost seamlessly.

Doorkeeper setup

bundle install
rails g doorkeeper:install
rails g doorkeeper:migration

(f81a96d)

After installing Doorkeeper we need to change its config to fit our needs. Since we use Devise, this will be our resource_owner_authenticator:

# config/initializers/doorkeeper.rb
resource_owner_authenticator do
  current_user || warden.authenticate!(scope: :user)
end

We also know that we'll always be dealing with a trusted application and we don't need our users to accept it the first time they log in, so we set it to skip authorization unconditionally. Of course, this might not be the case for you.

# config/initializers/doorkeeper.rb
skip_authorization do |resource_owner, client|
  true
end

Now is a good time to create an OAuth application for our front-end. Head to the http://localhost:3000/oauth/applications and create one:

Create OAuth application - step 1

Create OAuth application - step 2

Take a note of Application Id. We will need to pass it to our front-end. At Monterail, we do this by injecting JsEnv concern - this concept was explained very well by Dariusz Gertych in his extremely helpful article - 5 tips on how to use AngularJS with Rails that changed how we work. You can do it any way you like, just make sure JavaScript has access to this.

# config/application.yml
HOST: 'localhost:3000'
APPLICATION_ID: '93632f4c75569138fc68611c035eb060b564aa227c58ae178975d83d8f8bc239'

# app/controllers/concerns/js_env.rb

data = {
  host: ENV['HOST'],
  application_id: ENV['APPLICATION_ID']
}

(646d093)

We won't be needing a secret because there's no way to keep it… well, secret, in a JavaScript application.

Grape-doorkeeper

Let's look at our current, authentication-free API:

module API
  module V1
    class Users < Grape::API
      include API::V1::Defaults

      resource :users do
        desc "Return all users' emails, doesn't require authentication"
        get '/' do
          User.all.pluck(:email)
        end

        desc 'Return current user, requires authentication'
        get 'me' do
          'This will return current user in the near future'
        end
      end
    end
  end
end

I believe it's pretty straightforward. Now we need to tell Grape that each endpoint requires authorization. We'll also define some helpers in order to use them later. It's generally a good idea to move it to Defaults or some other shared module, but - since we have only one resource to protect - I left it here for the sake of simplicity.

class Users < Grape::API
  include API::V1::Defaults
  doorkeeper_for :all

  helpers do
    def current_token; env['api.token']; end

    def current_resource_owner
      User.find(current_token.resource_owner_id) if current_token
    end
  end

  desc 'Return current user, requires authentication'
    get 'me' do
      current_resource_owner
    end
  end
end

We also can use protected: false to skip authentication:

get '/', protected: false do

(210920b)

That's it for the back-end! Now, we bring the real fun stuff: Angular and its interceptors.

Angular

AccessToken

Let's create a simple service to store and retrieve our access token. It uses $localStorage to store it in a user's browser, but any cookie/storage solution will work.

# app/assets/javascripts/services/access-token.coffee
app = angular.module('myApp')
app.service 'AccessToken', ($localStorage, $timeout) ->
  get:         -> $localStorage.token
  set: (token) -> $localStorage.token = token
  delete:      -> delete $localStorage.token

TokenInterceptor

Wouldn't it be nice to automatically attach our access token to the requests we make? It's easily done using interceptor:

# app/assets/javascripts/init.coffee
app = angular.module('myApp')
app.config ($httpProvider) ->
  $httpProvider.interceptors.push('tokenInterceptor')

app.factory 'tokenInterceptor', (AccessToken, Rails) ->
  request: (config) ->
    # Send AccessToken only to our API
    if config.url.indexOf("//#{Rails.host}") == 0
      token = AccessToken.get()
      config.headers['Authorization'] = "Bearer #{token}" if token

    config

Catching and caching the AccessToken

Our interceptor won't work unless it has some Token to attach to. After a successful login, the user is redirected to this path:

/access_token=TOKEN&token_type=bearer&expires_in=999.

It means that we must create a route, then retrieve and save the token. Our AccessToken service doesn't have the ability to expire our token, but it's generally a good idea to implement this.

 $stateProvider
  .state 'accessToken',
    url: '/access_token=:response'
    controller: ($state, $stateParams, AccessToken) ->
      token = $stateParams.response.match(/^(.*?)&/)[1]
      AccessToken.set(token)

      $state.go 'index'

AuthCtrl

After all of this work it's finally time to create AuthCtrl to send the user to our login form. Remember that both client_id and redirect_uri must match with the application we created earlier.

# app/assets/javascripts/controllers/auth-ctrl.coffee
app = angular.module('myApp')

# Rails is our js_env object mentioned before
app.controller 'AuthCtrl', ($scope, AccessToken, Rails) ->
  $scope.loginUrl = "//#{Rails.host}/oauth/authorize?response_type=token&client_id=#{Rails.application_id}&redirect_uri=http://#{Rails.host}"
# some template (.slim)
div ng-controller="AuthCtrl"
  a ng-href="{{ loginUrl }}" Login

(e5f28c7)

That's it for the basic functionality - the user, after a successful login, is redirected back with a token that Angular can save. There are still two more things we should do though:

Logout

# app/assets/javascripts/controllers/auth-ctrl.coffee
$scope.logout = ->
  User.logout().then ->
    AccessToken.delete()
    setLoggedIn false
    $state.go 'index'

setLoggedIn = (isLoggedIn) ->
  $scope.loggedIn = !!isLoggedIn

setLoggedIn AccessToken.get()
# .slim
button.oauth__link.oauth__link--logout(
  type="button"
  ng-click="logout()"
  ng-show="loggedIn"
) Logout
# app/controllers/api/v1/users.rb
helpers do
  def warden; env['warden']; end
end

desc 'Logout user'
delete 'logout' do
  warden.logout
end

(8f7c4ff)

401 interceptor

Sooner or later Rails will return you the 401 (unauthorized) error code. It's good to do something with it. We might, for example, redirect the unauthorized user to a special page, explain what happened and suggest to log in.

# app/assets/javascripts/routes.coffee
$stateProvider
  .state '401',
    url: '/unauthorized'
    controller: ($state, AccessToken) ->
      $state.go 'index' if AccessToken.get()
    templateUrl: '401.html'
# app/assets/javascripts/init.coffee
app.config ($httpProvider) ->
  $httpProvider.responseInterceptors.push('unauthorizedInterceptor')

app.factory 'unauthorizedInterceptor', ($q, $injector) ->
  return (promise) ->
    success = (response) -> response
    error   = (response) ->
      if response.status == 401
        $injector.get('$state').go('401')
      $q.reject(response)

    promise.then success, error

(be2e60c)

Summary

That's it for now! I hope this post was helpful for you. OAuth Implicit Grant is probably not something you will be working on every day but when it finally presents itself, this article will have you covered.

Thank you and - as always - we'd love to hear your feedback.

Meet Ruby on Rails experts

We’ve been doing Ruby on Rails since 2010 and our services quality and technical expertise are confirmed by actual clients. Work with experts recognized as #1 Ruby on Rails company in the World in 2017 by Clutch.

Talk to our team and confidently build your next big thing.

Adam Hodowany avatar
Adam Hodowany