Contact

Please send a message if you want to work together or if you have any questions.

Token Based Authentication with JWT, Ember and Rails


TLDR: We'll build an Ember app to allow users to sign up, login, and see a list of secrets when they are authenticated. The back end code is on github here, and the client side code is over here.

Let's start with the backend code.

Rails 5 has not shipped yet so I am going to clone the master branch and use the new --api option to create a lightweight api.

$ git clone https://github.com/rails/rails
$ cd rails
$ bundle exec railties/exe/rails new ~/<project_path>/authorizer --api --database=postgresql

These commands will create a new api only rails app called authorizer in the <project_path> you specify. Now cd into the project and update your Gemfile with these gems.

source 'https://rubygems.org'

gem 'rails', '>= 5.0.0.beta3', '< 5.1'
gem 'pg', '~> 0.18'
gem 'puma', '~> 3.0'
gem 'bcrypt', '~> 3.1.7'
gem 'active_model_serializers', '~> 0.10.0.rc4'
gem 'rack-cors'
gem 'jwt', '~> 1.5', '>= 1.5.3'

group :development do
gem 'listen', '~> 3.0.5'
gem 'spring'
gem 'spring-watcher-listen', '~> 2.0.0'
gem 'faker'
end

Run bundle install

Let's create our User model and a Secret model which will contain the data that authenticated users can access.

$ rails g resource user username:string password_digest:string
$ rails g resource secret text:string

Run rails db:create db:migrate

Let's also add seed data to the Secret model in our db/seeds.rb file.

20.times do
Secret.create(text: Faker::Hipster.sentence)
end

Run rails db:seed

Our User model will contain some some basic validations and the has_secure_password method to let us authenticate with a bcrypt password.

class User < ApplicationRecord
has_secure_password
validates :username, presence: true, uniqueness: true
end

Now create a Sessions controller which will handle logins.

$ rails g controller sessions

And setup our routes in config/routes.rb. We'll just need the login route, a signup and index route in the Users controller, and an index route for the Secrets.

Rails.application.routes.draw do
post 'login', to: 'sessions#login'
resources :users, only: [:index, :create]
resources :secrets, only: :index
end

Before we implement the signup/login actions, let's create a class to handle the JSON web token logic in lib/json_web_token.rb. Note: add config.autoload_paths += %W(#{config.root}/lib) to application.rb so the file gets loaded.

class JsonWebToken

def self.encode(payload)
JWT.encode(payload, Rails.application.secrets.secret_key_base)
end

def self.decode(token)
decoded_token = JWT.decode(token, Rails.application.secrets.secret_key_base).first
HashWithIndifferentAccess.new(decoded_token)
rescue
nil # JWT throws error for null tokens
end
end

Next we can move onto the signup route, which will be the create action in the Users controller. Note I'm adding a skip_before_action for two methods that will be implemented in the Application controller.

class UsersController < ApplicationController
skip_before_action :set_current_user, :authenticate_request, only: :create

def index
if params[:me].present?
users = User.find(@current_user.id)
else
users = User.all
end
render json: users
end

def create
user = User.new(username: params[:username], password: params[:password])
if user.save
render json: user, status: :created
else
render json: { errors: user.errors }, status: :unprocessable_entity
end
end
end

With this create action we will be able to send a POST request to /users with a username and password to signup a new account. This action creates a new user but does not log her in, rather an additional request will be made from the client app immediately after the signup form is submitted to make a login request. The index action will be used later in the Ember app to find the current user. Let's implement that login action next in the Sessions controller.

class SessionsController < ApplicationController
skip_before_action :set_current_user, :authenticate_request

def login
user = User.find_by_username(params[:username])
if user && user.authenticate(params[:password])
token = JsonWebToken.encode(user_id: user.id)
# ember-simple-auth needs token in 'access_token' key for oauth2
render json: { access_token: token }
else
render json: { errors: "Invalid username or password" }, status: :unauthorized
end
end
end

So to login a user, a POST request is made to /login with a username and password. If a user is found by that username and the bcrypt authenticate() method succeeds, we can create a token to send back to the client. The token is encoded with the user's id, that way we can identify and authenticate the current user in subsequent api requests.

Now we can implement those methods to find and authenticate users in the Application controller.

class ApplicationController < ActionController::API
before_action :set_current_user, :authenticate_request

private

def set_current_user
if decoded_auth_token
@current_user = User.find(decoded_auth_token[:user_id])
end
end

def decoded_auth_token
if request.headers['Authorization'].present?
token = request.headers['Authorization'].split(' ').last
JsonWebToken.decode(token)
end
end

def authenticate_request
unless @current_user
render json: { errors: "Not Authorized" }, status: :unauthorized
end
end
end

Now every request made to the api will search for a token in the http Authorization header, decode that token, and attempt to load the user from the user id that was originally encoded into the token. Without a token or user found, the request will render an unauthorized error response.

Also, we have to allow cross origin requests to be made to the api, and we can do that with the rack-cors gem by modifying config/initializers/cors.rb

Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins '*'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options, :head]
end
end

This setup allows any request to be made to any resource from any origin. It production this should be limited to the origin of your client side app.

Last on the api side, we can implement the Secrets index action. Let's also edit serializers/secret_serializer.rb so the json response contains the secret's text.

class SecretsController < ApplicationController

def index
secrets = Secret.all
render json: secrets
end
end
class SecretSerializer < ActiveModel::Serializer
attributes :text
end

I am going to build our api as a JSONApi vs a RESTApi, and the active_model_serializer gem provides an adapter to make this easy. Just add this initializer to config/initializers/active_model_serializer.rb

ActiveModelSerializers.config.adapter = :json_api

Now let's get started with Ember!

$ ember new authorizer

First some configuration steps. I recommend building the Ember app with pod structure, so to configure ember-cli to do so, add the usePods option to .ember-cli

{
"disableAnalytics": false,
"usePods": true
}

With pod structure in place, we can delete the controllers, models, routes, and templates directories in /app. After doing that, create a simple application template in app/application/template.hbs

{{outlet}}

Followed by the default index template in app/index/template.hbs

<h1>Index</h1>

At this time you should be able to run $ ember server and see the index template at http://localhost:4200

To handle the authentication process in the Ember app, we can use the great npm package ember-simple-auth. Run the install command to add it to your app.

$ ember install ember-simple-auth

Note: at the time of writing, I installed ember-simple-auth@1.1.0-beta.3 to remove deprecation warnings.

Before we get to the signup form, let's set up a simple Ember user model.

$ ember g model user
import DS from 'ember-data';
export default DS.Model.extend({
username: DS.attr('string')
});

Now let's create the login and signup routes for the Ember app in app/router.js

import Ember from 'ember';
import config from './config/environment';

const Router = Ember.Router.extend({
location: config.locationType
});

Router.map(function() {
this.route('login');
this.route('signup');
});

export default Router;

And then create templates for the two routes in app/signup/template.hbs and app/login/template.hbs

<h1>Sign up</h1>

<form {{action "register" on="submit"}}>
{{#if errorMessage}}
<p>
<strong>Error:</strong>
<code>{{errorMessage}}</code>
</p>
{{/if}}

<div>
<label for="username">Username</label>
{{input value=username placeholder="Enter username"}}
</div>

<div>
<label for="password">Password</label>
{{input value=password placeholder="Password" type="password"}}
</div>

<button type="submit">Sign up</button>
</form>
<h1>Login</h1>

<form {{action "authenticate" on="submit"}}>
{{#if errorMessage}}
<p>
<strong>Error:</strong>
<code>{{errorMessage}}</code>
</p>
{{/if}}

<div>
<label for="username">Username</label>
{{input value=username placeholder="Enter username"}}
</div>

<div>
<label for="password">Password</label>
{{input value=password placeholder="Password" type="password"}}
</div>

<button type="submit">Login</button>
</form>

Then if you update app/application/template.hbs to show links to the routes, you will be able to transition between the pages.

<nav>
{{#link-to "index"}}
Index
{{/link-to}}
{{#link-to "login"}}
Login
{{/link-to}}
{{#link-to "signup"}}
Sign up
{{/link-to}}
</nav>
{{outlet}}

To start creating our signup and login actions, we will first add the oauth2 strategy for ember-simple-auth. Create app/authenticators/oauth2.js

import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';
import config from '../config/environment';
import Ember from 'ember';

export default OAuth2PasswordGrant.extend({
serverTokenEndpoint: config.apiURL + '/login'
});

Note: In config/environment.js I added the api endpoint as an ENV variable in the development block to point to the local rails app.

if (environment === 'development') {
ENV.apiURL = 'http://127.0.0.1:3000';
}

Then after the authenticator we can setup the associated authorizer for oauth in app/authorizers/oauth2.js

import OAuth2BearerAuthorizer from 'ember-simple-auth/authorizers/oauth2-bearer';
import Ember from 'ember';

export default OAuth2BearerAuthorizer.extend();

When a user is authenticated, we will also want to load that user from the back end. So for that process we can create an ember service.

$ ember g service session-account
import Ember from 'ember';

export default Ember.Service.extend({
session: Ember.inject.service(),
store: Ember.inject.service(),

loadCurrentUser() {
if (this.get('session.isAuthenticated')) {
this.get('store')
.queryRecord('user', { me: true })
.then((user) => {
this.set('currentUser', user);
});
}
}
});

The loadCurrentUser() function queries the api at /users?me=true. The 'user#index' action in our rails app will see the params[:me] value and respond with the @current_user.

We will want loadCurrentUser() to always run first when the application is loaded or the page is refreshed, so let's add that to app/application/route.js

import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';

export default Ember.Route.extend(ApplicationRouteMixin, {
sessionAccount: Ember.inject.service(),

beforeModel() {
this.get('sessionAccount').loadCurrentUser();
}
});

Note: the ApplicationRouteMixin provided by ember-simple-auth will also handle some session events for us automatically, such as refreshing the page on logout so the user's sessions is fully terminated. Speaking of logging out, we can add that function to app/application/controller.js

import Ember from 'ember';

export default Ember.Controller.extend({
session: Ember.inject.service(),
sessionAccount: Ember.inject.service(),

actions: {
logout() {
this.get('session').invalidate();
}
}
});

We are finally ready to create the signup registration function. I am going to use ember-ajax which is a wrapper around jQuery.ajax() that integrates nicely with Ember. First we can specify the default api host option for ember-ajax in app/services/ajax.js

import AjaxService from 'ember-ajax/services/ajax';
import config from '../config/environment';

export default AjaxService.extend({
host: config.apiURL,
});

Then our register function can be added to app/signup/controller.js

import Ember from 'ember';

export default Ember.Controller.extend({
session: Ember.inject.service(),
sessionAccount: Ember.inject.service(),
ajax: Ember.inject.service(),

actions: {
register() {
let userData = {
username: this.get('username'),
password: this.get('password')
};

this.get('ajax').post('/users', { data: userData })
.then(() => {
let { username, password } = this.getProperties('username', 'password');
this.get('session').authenticate('authenticator:oauth2', username, password)
.then(() => {
this.get('sessionAccount').loadCurrentUser();
});
}).catch(() => {
this.set('errorMessage', 'An error occurred, please try again');
});
}
}
});

register() will make a POST request to /users, and if it succeeds the user will be logged in, and then the sessionAccount service will load the user. The catch function here is displaying a generic error message, but in production some more validations and descriptive messages should be added to let the user know if the username is already taken, or if the password is outside of the allowed character limit.

Let's also add the authenticate() function to app/login/controller.js

import Ember from 'ember';

export default Ember.Controller.extend({
session: Ember.inject.service(),
sessionAccount: Ember.inject.service(),

actions: {
authenticate() {
let { username, password } = this.getProperties('username', 'password');
this.get('session')
.authenticate('authenticator:oauth2', username, password)
.then(() => {
this.get('sessionAccount').loadCurrentUser();
}).catch((reason) => {
this.set('errorMessage', 'Invalid username or password');
});
}
}
});

You can see the process here is similar to what happens during the signup process. We use ember-simple-auth's session authenticate method to make a request to /login and if that succeeds we call the same loadCurrentUser() function.

To fit our client code to fit into the back end, we also have to set up an adapter for ember-simple-auth. Create app/application/adapter.js and add the following code:

import config from '../config/environment';
import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';

export default DS.JSONAPIAdapter.extend(DataAdapterMixin, {
host: config.apiURL,
authorizer: 'authorizer:oauth2'
});

To see this all in action, let's update app/application/template.hbs

<nav>
{{#link-to "index"}}
Index
{{/link-to}}
{{#if session.isAuthenticated}}
<span>{{sessionAccount.currentUser.username}}</span>
<button {{action "logout"}}>Logout</button>
{{else}}
{{#link-to "login"}}
Login
{{/link-to}}
{{#link-to "signup"}}
Sign up
{{/link-to}}
{{/if}}
</nav>

{{outlet}}

Now you should be able to sign up, login, logout, and see the current user's username in the application template.

The last thing we want to do is setup a model and corresponding route where authenticated user's can see our super-secret list of secrets.

$ ember g model secret
$ ember g route secret

Add the secret route to app/application/template.hbs

  ...
{{#link-to "index"}}
Index
{{/link-to}}
{{#link-to "secret"}}
Secrets
{{/link-to}}
{{#if session.isAuthenticated}}
...

And setup the model in app/secret/model.js

import DS from 'ember-data';

export default DS.Model.extend({
text: DS.attr('string')
});

Then we'll load the model data in app/secret/route.js. Here we can add the AuthenticatedRouteMixin from ember-simple-auth, which means this route will only be accessible for authenticated users.

import Ember from 'ember';
import AuthenticatedRouteMixin from 'ember-simple-auth/mixins/authenticated-route-mixin';

export default Ember.Route.extend(AuthenticatedRouteMixin, {
model() {
return Ember.RSVP.hash({
secrets: this.store.findAll('secret')
});
}
});

Now create the corresponding template in app/secret/template.js so we can see the secrets

<ul>
{{#each model.secrets as |secret|}}
<li>{{secret.text}}</li>
{{/each}}
</ul>

Before we are done, let's add one more configuration option provided by ember-simple-auth. By default, authenticate() will transition the user to /index after successful authentication, but we can change that by adding the following to config/environment.js

ENV['ember-simple-auth'] = {
routeAfterAuthentication: 'secret'
};

With that in place, after users sign up or login they will be automatically redirected to the secret route.

That's it, you should now have a fully functioning api and client side app to store your app's secrets. Once again, the Rails code is on github here, and the Ember code over here!