Recently I was interested in implementing token-based authentication with a decoupled frontend and backend. I googled about ways to do it the Angular-Rails way. Turns out it is not that simple and straightfoward. After a couple of days of reading fragmented sources of information on the web, I finally rolled my own authentication system.
Granted that one could easily use Devise gem for their authentication needs, however there’s too much cruft associated with it and I want to implement authentication from the bottom-up to solidify my understanding.
Given that the resources are scattered out there, I thought I’d write a tutorial to synthesize them into a coherent whole for the benefit of the reader. A large chunk of my tutorial (the Rails backend) is adapted from Michael Hartl’s famous RoR Tutorial, with the rest culled from various resources like StackOverflow and other blogs.
This tutorial is for those who want to understand how token-based authentication works. We’ll be rolling a simple solution that authenticates users based on their email and password. Log-ins, sign-outs and simple authorization are included as well. I stress that the auth system in this tutorial is definitely not industrial-grade but it won’t be prone to easy hack-ins either.
You may want to read this article for a rundown of the evolution of API authentication and this article to understand how token-based auth compares with the traditional cookie-based auth.
We’ll be using token-based auth in this tutorial because it is more conventional and confers more advantages compared to cookie-based auth.
Even though this tutorial is mainly for developers using Angular and Rails, the process of token-based authentication is generic enough such that a framework-agnostic flow can be gleaned from it:
Without further ado, let’s get started.
We will be starting a Rails API server from scratch. Run rails new sample-api
in terminal.
We will be using bcrypt gem as the backbone of our auth machinery. Hence we have to uncomment bcrypt-ruby gem in Gemfile and then do bundle install
.
gem 'bcrypt-ruby', '~> 3.1.2'
Next, we create our User model. It will have these attributes: name, email, password digest and remember token. We will not be storing passwords in plaintext as hackers can easily retrieve user login credentials through the database. Create the User model by running this command:
rails g model User name:string email:string password_digest:string remember_token:string
In the database migration file auto-generated by the last command, add in a remember token index for easy retrieval since we will be accessing users by their token often.
class CreateUsers < ActiveRecord::Migration
def change
create_table :users do |t|
t.string :name
t.string :email
t.string :password_digest
t.string :remember_token
t.timestamps
end
add_index :users, :remember_token
end
end
Run rake db:migrate
to update the database schema. Your schema should now look like this:
ActiveRecord::Schema.define(version: 20140801074722) do
create_table "users", force: true do |t|
t.string "name"
t.string "email"
t.string "password_digest"
t.string "remember_token"
t.datetime "created_at"
t.datetime "updated_at"
end
add_index "users", ["remember_token"], name: "index_users_on_remember_token"
end
Next, we modify our User model class in models/user.rb
to utilise the bcrypt gem by adding has_secure_password
. We will also use some boilerplate from Michael Hartl’s Rails Tutorial (thanks Michael) to validate user’s credentials. Class-level methods used to generate and digest tokens are added as well. Finally, we define a private instance-level method to assign the newly generated token to the remember_token
field of User
model.
class User < ActiveRecord::Base
has_secure_password # using bcrypt
# boilerplate from Hartl's tutorial to validate user input
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true,
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, length: { minimum: 6 }
before_create :create_remember_token
# class-level methods to generate and digest tokens
def User.new_remember_token
SecureRandom.urlsafe_base64
end
def User.digest(token)
Digest::SHA1.hexdigest(token.to_s)
end
# private instance-level method to assign token
private
def create_remember_token
self.remember_token = User.digest(User.new_remember_token)
end
end
Next, we move on to the controllers. Before creating the users controller, we have to configure the application controller. Since we are decoupling the frontend and the backend, each of them lives in a separate domain. Hence the frontend app have to make cross-domain requests to the Rails backend. Such requests will not be acknowledged unless the server is configured to implement cross-origin resource sharing (CORS).
As the application controller is the main controller, changes made to it affect the rest of the controllers which inherit from it. Hence it makes sense to implement CORS in application controller.
Since we are not sending the auth token on the client side through the custom X-CSRF-Token header, we have to change protect_from_forgery
to use :null_session
along with a guard to handle CSRF token verification issue. We included respond_to :json as the Rails API will be mainly serving JSON responses.
We have also included SessionsHelper
which contains helper methods (to be implemented later) for sign-ins and sign-outs. Finally, there is a callback method used to restrict access to unauthorized users.
class ApplicationController < ActionController::Base
# Prevent CSRF attacks by raising an exception.
# For APIs, you may want to use :null_session instead.
protect_from_forgery with: :null_session, if: Proc.new { |c| c.request.format == 'application/json' }
include SessionsHelper # include helper methods used for sign in and sign out
respond_to :json
before_filter :set_cors_headers
before_filter :cors_preflight
def set_cors_headers
headers['Access-Control-Allow-Origin'] = '*'
headers['Access-Control-Allow-Methods'] = 'GET, POST, PUT, DELETE, OPTIONS'
headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
headers['Access-Control-Max-Age'] = '3628800'
end
# before every actual request, the browser sends a OPTIONS request
# to check if the server implements CORS
# for such requests, send an empty response with 200 status
def cors_preflight
head :ok if request.method == 'OPTIONS'
end
# callback method used for authorization
private
def restrict_access
if request.headers['HTTP_AUTHORIZATION']
token = request.headers['HTTP_AUTHORIZATION'].split(" ").second
user = User.find_by(remember_token: User.digest(token))
end
head :unauthorized unless user # send 401 response unless user is authorized
end
end
To implement sign-up, we have to create a Users controller first as this is where we define the creation of new users. Run rails g controller Users
in terminal.
Define the create
method in controllers/users_controller.rb
as shown below:
class UsersController < ApplicationController
def create
user = User.new(user_params)
if user.save
token = sign_in user
render status: 200,
json: {
success: true,
info: "Signed up successfully",
data: {
auth_token: token
}
}
else
render status: 200,
json: {
success: false,
info: "Signed up failed, user exists",
data: {}
}
end
end
private
def user_params
params.require(:user).permit!
end
end
The alert reader can see that sign_in
method is not defined in this class; it is defined in SessionsHelper
module instead. Since we want the user to be automatically signed in after registration, we have to proceed with the implementation of sign-in process, and followed by sign-out. This is done with sessions. A session is considered as a resource in the server, hence we have to create a controller for it.
Run rails g controller Sessions
to create the SessionsController
. As a side-effect, SessionsHelper
is automatically created too.
Let’s define the create
and destroy
methods in controllers/sessions_controller.rb
:
class SessionsController < ApplicationController
def create
user = User.find_by(email: params[:user][:email].downcase)
if user && user.authenticate(params[:user][:password])
#sign_in helper method from SessionsHelper
token = sign_in user
render status: 200,
json: {
success: true,
info: "Logged in successfully",
data: {
auth_token: token
}
}
else
render status: 200,
json: {
success: false,
info: "Log in failed",
data: {}
}
end
end
def destroy
if request.headers['HTTP_AUTHORIZATION']
token = request.headers['HTTP_AUTHORIZATION'].split(" ").second
user = User.find_by(remember_token: User.digest(token))
end
if user
#sign_out helper method from SessionsHelper
sign_out user
render status: 200,
json: {
success: true,
info: "Signed out successfully",
data: {}
}
else
render status: 200,
json: {
success: false,
info: "Signed out failed, no such user",
data: {}
}
end
end
end
Looking at the above code, there’s nothing out of the ordinary except for this snippet user.authenticate(params[:user][:password])
. The authenticate()
method is part of the bcrypt gem, and is used to digest the given password to compare it with the stored password digest in the users database, returning a boolean value.
We also observe that we invoke the helper methods from the SessionsHelper
module in helpers/sessions_helper.rb
. Without further ado, let’s flesh the module out.
module SessionsHelper
def sign_in(user)
remember_token = User.new_remember_token
user.update_attribute(:remember_token, User.digest(remember_token))
remember_token
end
def sign_out(user)
user.update_attribute(:remember_token, User.digest(User.new_remember_token))
end
end
Let’s define a restricted API call to illustrate the authorization concept. We make it such that only logged-in users can access the index of all users from the API.
We return to controllers/users_controller.rb
to effect this change by defining the index method and using a before_action
callback hook to restrict access to it.
class UsersController < ApplicationController
before_action :restrict_access, only: :index
def index
respond_with User.all
end
# rest of the code here
The restrict_access
method is defined in ApplicationController
and UsersController
is a subclass of it. That’s why we are able to invoke restrict_access
method here.
The index
method will respond with a JSON containing all users, this is made possible by the respond_to :json
method in ApplicationController
.
This is the final part of our Rails API. We have to tie up the loose ends by connecting the methods in our controllers to the API endpoints – the URLs.
Rails.application.routes.draw do
resources :users, defaults: {format: 'json'}
match '/users', to: 'users#set_cors_headers', via: :options
resources :sessions
match '/signin', to: 'sessions#create', via: [:post, :options]
match '/signout', to: 'sessions#destroy', via: [:delete, :options]
match '/signup', to: 'users#create', via: [:post, :options]
end
We have to take care of CORs preflight by specifying that the API endpoints can respond to OPTIONS request.
Notice that even though we defined every possible RESTful endpoints for /users
using resources :users, defaults: {format: 'json'}
, we still have to handle the OPTION request for /users
by directing it to set_cors_headers
method defined in ApplicationController
.
To be honest, I’m not sure whether my approach is considered best practice or not. Either way, it doesn’t look elegant. If you have any better solutions, do let me know!
This concludes the set-up for Rails API.
We will be creating a sample app using Angular framework. I’ll assume that the reader has already scaffolded the app with Yeoman.
If you haven’t already, please follow this codelab until Step 5.
We will need to configure our Angular app by adding a dependency to store auth tokens on the client side.
First, we run bower install --save angular-local-storage
in terminal to install the local storage module.
We then have to edit the application module in scripts/app.js
to include the local storage module:
var app = angular
.module('sampleAppApp', [
'ngAnimate',
'ngCookies',
'ngResource',
'ngRoute',
'ngSanitize',
'ngTouch',
'LocalStorageModule'
]);
app.config(['localStorageServiceProvider', function(localStorageServiceProvider){
localStorageServiceProvider.setPrefix('ls');
}]);
Just a caveat: Yeoman named my app as sampleAppApp
because I ran yo angular
in a folder named sample-app
. Please replace sampleAppApp
with the name that Yeoman generated for you.
Notice that we assigned the application module to a variable app. This is used to easily refer to the application module later on.
We also have to configure the local storage module by setting its prefix to avoid namespace conflicts.
Moving on, we lay out the routings in the application module:
app.config(function ($routeProvider) {
$routeProvider
.when('/', {
templateUrl: 'views/main.html',
controller: 'MainCtrl'
})
.when('/about', {
templateUrl: 'views/about.html',
controller: 'AboutCtrl'
})
.when('/login', {
templateUrl: 'views/login.html',
controller: 'LoginCtrl'
})
.when('/signup', {
templateUrl: 'views/signup.html',
controller: 'SignupCtrl'
})
.when('/signout', {
templateUrl: 'views/signout.html',
controller: 'SignoutCtrl'
})
.when('/restricted', {
templateUrl: 'views/users.html',
controller: 'UsersCtrl'
})
.otherwise({
redirectTo: '/'
});
})
In addition to the two default routes created by Yeoman, we now have four routes, with one route being restricted only to authorized users.
Having defined the routings, we proceed to flesh out the rest of the app by creating the controllers and the views.
We start with the sign-up controller by creating a new file in scripts/controllers/signup.js
:
app.controller('SignupCtrl', function($location, $scope, $http, localStorageService) {
$scope.user = {};
if (localStorageService.get('token')) {
$scope.info = "Logged in" ;
}
$scope.signup = function() {
console.log($scope.user);
$http({
url: 'http://localhost:3000/signup',
method: 'POST',
data: {
user: $scope.user
}
}).success(function(response){
$scope.info = response.info;
localStorageService.set('token', response.data.auth_token)
});
};
});
In this controller, we make a POST request containing user credentials to the Rails API (port number 3000). If successful, the API will return an auth token which will be stored in the local storage. We also display the relevant information in the API response so that we know whether the sign-up is successful or not.
Now we need to create a sign-up form to carry out form-based authentication. Let’s create a new file in views/signup.html
and add the HTML as follows:
<form ng-submit="signup()" class="well">
<h3>Sign up</h3>
<input ng-model="user.name" type="text" placeholder="Name">
<input ng-model="user.email" type="text" placeholder="Email">
<input ng-model="user.password" type="password" placeholder="Password">
<input ng-model="user.password_confirmation" type="password" placeholder="Confirm password">
<button type="submit" class="btn">Sign up</button>
</form>
<div>
{ { info } }
<!-- please remove spacing between curly brackets! -->
</div>
Our user credentials consist of four parameters: name, email, password and password confirmation. The last parameter may seem optional, but it is required by bcrypt to generate a password digest.
We have also included a string interpolation enclosed in a div block to display result of the log-in process. As Markdown is unable to escape double curly brackets, I deliberately added a space between them. You have to remove the spacing to make Angular’s string interpolation work.
At this point you may wish to test out the app and API by running grunt serve
and rails s
respectively. But don’t forget to include the signup.js
file in the index.html.
If there’s no problem, perfect! Moving on, we will create the log-in controller (scripts/controllers/login.js
):
app.controller('LoginCtrl', function($location, $scope, $http, localStorageService) {
if (localStorageService.get('token')) {
$scope.info = "Logged in";
}
$scope.login = function() {
$http({
url: 'http://localhost:3000/signin',
method: 'POST',
data: {
user: $scope.user
}
}).success(function(response){
$scope.info = response.info;
localStorageService.set('token', response.data.auth_token)
});
};
});
The log-in controller is more or less the same as sign-up controller. The difference lies in the parameters passed to API. Log-in process is simpler, requiring only two parameters: email and password. The email is used to find the user, and the password is used for authentication. Let’s flesh out the form used to input the log-in parameters in views/login.html
:
<form ng-submit="login()" class="well form-inline">
<h3>Login</h3>
<input ng-model="user.email" type="text" placeholder="Email">
<input ng-model="user.password" type="password" placeholder="Password">
<button type="submit" class="btn">Log in</button>
<button ng-click="signup()" class="btn">Sign up</button>
</form>
<div>
{ { info } }
<!-- please remove spacing between curly brackets! -->
</div>
Finally, we complete the authentication cycle by fleshing out the sign-out controller in scripts/controllers/signout.js
:
app.controller('SignoutCtrl', function($location, $scope, $http, localStorageService) {
$scope.signout = function() {
$http({
url: 'http://localhost:3000/signout',
method: 'DELETE',
data: {}
}).success(function(response){
$scope.info = response.info;
localStorageService.remove('token')
});
};
$scope.signout();
});
Here, we make a DELETE request to the API, if successful, we clear the token in local storage and return the relevant information to be displayed.
The sign-out view (views/signout.html
) is a just a HTML file to display the result:
<div>
{ { info } }
<!-- please remove spacing between curly brackets! -->
</div>
The alert reader may notice that we didn’t pass anything to the body of the DELETE request. So how do the API know which user to delete the session for?
Even though the authentication cycle is compete, we still have to construct the underlying pipes of our Angular app to make it work.
The idea is that the HTTP request headers should contain the token every time we make a request. How do we do that?
Turns out Angular has a built-in service to modify the HTTP request message before it is being sent to the server. We shall use a subset of the $http service
– HTTP interceptors – for this purpose.
Let’s create a file named auth-interceptor.js
in scripts/services
folder (create the folder if you haven’t) and add the code as shown below:
app.factory('AuthInterceptor', function(localStorageService, $q, $location) {
return {
request: function(config) {
config.headers = config.headers || {};
if (localStorageService.get('token')) {
config.headers.Authorization = 'Bearer ' + localStorageService.get('token');
}
return config || $q.when(config);
},
response: function(response) {
return response || $q.when(response);
},
responseError: function(rejection) {
if (rejection.status == 401) {
$location.path("/login");
}
return $q.reject(rejection);
}
};
});
app.config(function ($httpProvider) {
$httpProvider.interceptors.push('AuthInterceptor');
});
The idea is simple. For every HTTP request, if the token exists in local storage, we set the token in the HTTP Authorization header. Otherwise the header would not be set and a 401 response will be expected when we tried to access a restricted page.
We also deal with intercepting responses here. For every responses with status 401 (meaning unauthorized), we direct the user to the login page.
All in all, auth-interceptor.js
essentially takes care of the authorization process for you.
Having done the necessary plumbing to make authentication work. We will create a restricted view that only logged-in users can access. The restricted view will contain a raw JSON response of all the user data.
First, we create a controller to fetch the user data from the API in scripts/controllers/users.js
:
app.controller('UsersCtrl', function($scope, $http) {
$http.get('http://localhost:3000/users').success(function(data) {
$scope.users = data;
});
});
We are calling the restricted API endpoint here. If the user is authorized, display the data, otherwise the user will be redirected to the log-in page by auth-interceptor.js
.
To finally create the restricted view (views/users.html
) is very simple. Just add the string interpolations as shown below:
<div>
{ { users } }
<!-- please remove spacing between curly brackets! -->
</div>
You may notice that we didn’t create any links for our routes; we have to type in the URL path manually in the address bar every time we want to test the auth. Implementing the links is left as an exercise for the diligent reader.
There is one more thing though. We have to tell Angular where to load up our controllers and services by adding those dependencies in index.html
:
<!-- build:js({.tmp,app}) scripts/scripts.js -->
<script src="scripts/app.js"></script>
<script src="scripts/controllers/main.js"></script>
<script src="scripts/controllers/about.js"></script>
<script src="scripts/controllers/login.js"></script>
<script src="scripts/controllers/signup.js"></script>
<script src="scripts/controllers/signout.js"></script>
<script src="scripts/controllers/users.js"></script>
<script src="scripts/services/auth-interceptor.js"></script>
<!-- endbuild -->
</body>
</html>
This concludes my tutorial finally.
To play with the auth, start the frontend and backend servers with grunt serve
and rails s
respectively and type any of the URLs in the address bar:
/signup
/login
/signout
/restricted
Have fun!