Some time ago we implemented authentication with OAuth Implicit Flow using:
- Grape - framework for REST API (our introduction to Grape),
- Doorkeeper - OAuth2 provider for Rails,
- Devise
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:
Demo
We've prepared a sample demo application with meaningful commits, so you can run it and play with it:
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
andvalidatable
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'
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
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:
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']
}
…
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
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
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
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
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.