JWT Auth in Rails, From Scratch

In an earlier post, we took a look at JWT authentication in Rails, and implemented a JWT authentication system using the Knock gem.

Here, we'll drop down a level and re-build our JWT authentication system from scratch, without the help of the Knock gem. You can check out the code for this post here, and you can check out the client-side portion of this project, written in Ember, here.

Let's get started!

Authentication in a Rails API

Authentication when working with CORS always presents an interesting challenge. In a normal (i.e. non-API) Rails app, we "log in" a user by storing their unique user ID in the session store. This means that authentication information is stored on the server side, in the session hash. In other words, our server becomes stateful, keeping track of whether or not a user is "logged in", and who that user is.

What happens in a Rails API, then, when the client is divorced from the server? Well, we'll need to tell the client, i.e. our Ember app, to store some kind of unique identifier and send that unique identifier to the Rails API with every request. Rails can then use the unique identifier, or token, to identify the user making the request.

This is the basic model of Ember Simple Auth. With the Ember Simple Auth add-on, we can authorize our user like this:

  • User "logs in" in via the browser, and Ember grabs the user's email and password and sends them to Rails, requesting authentication.
  • Rails looks up the user. If the user can be authenticated, Rails sends that user's unique token back to Ember.
  • Ember stores that token in the session store, and sends it back to Rails with every subsequent requests.
  • Rails receives any such requests, uses the token to look up the current user and return requested data back to Ember, the client.
  • When someone "logs out", Ember removes the token from the session store, and subsequent requests to rails do not include that token. Rails therefore is not able to find a "current user".

This token-based authentication system is described in an earlier post, if you want to learn more.

One downside of this approach, however, is that it necessitates generating a unique token for every user created, persisting that token in the database, and querying the database using that token every time we want to authenticate a user.

Wouldn't it be great if we could authenticate the user in a stateless manner, without storing tokens in the database? Wouldn't it be even greater if we could do that via requests that are compact and self-contained?

Well, lucky for us, there's JWT Authentication.

What is JWT Authentication?

JSON Web Token (JWT) Authentication is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted.*

In plain English, JWT allows us to authenticate requests between the client and the server by encrypting authentication information into a compact JSON object. Instead of, say, passing a user's own unique token (which we would need to persist to the database), or (god forbid), sending a user's raw email and password with every authentication request, JWT allows us to encrypt a user's identifying information, store it in a token, inside a JSON object, and include that object in every request that requires authentication.

Let's take a closer look at how that cycle might work, using the example of an Ember app + Rails API.

  • User fills out the log in form via the Ember app and hits "log in!"
  • Ember POSTs user's email and password to the Rails API.
  • Rails receives the POST request and and queries the database for the right user. If the user can be authenticated...
  • We'll use JWT to encrypt that user's unique ID into a compact and secure JSON Web Token.
  • This token is then included in the response that Rails sends back to Ember.
  • Ember stores the encrypted JWT token in local storage, retrieving it and sending it back to Rails, as the HTTP Authentication header in any authenticated requests.

So, what's so great about this system?

Well, for one thing, we are not storing a unique user token in our database. Instead, we are encrypting the user's unique identifying info in our token, and decrypting it on the Rails side when we need to identify the "current user". Secondly, our server is not responsible for keeping track of the current user, as is the case when we use Rails' session object.

If you're not yet convinced of how great this is, check out the jwt.io documentation. It offers some very clear and concise info.

JWT Encryption: How Does it Work?

JWT tokens are encrypted in three parts:

  1. The header: the meta-data describing the encryption algorithm and type of token
  2. The payload: the actual data concerning the user (id, email, etc.)
  3. The signature: special combo of header info + payload to ensure that the sender of the token is really you!

Let's take a look at an example, using the JWT Ruby library to encode our very own token!

Given this information:

  • {user_id: 1}
  • hmac secret: $39asdulawk3j489us39vm9370dmsZ
  • encryption algorithm: HS256

We can encrypt our token in the following way:

require 'jwt'

JWT.encode(  
  {user_id: 1}, 
   hmac,
   "H256")

And it will return our three part JWT:

QyJ0asdfjos.ald925lIn0.eyJ0ZXN0Ijas39uZGF0YSJ9.  

Similarly, to decode our token, we can use the following JWT Ruby code, where token is set equal to the above JWT, and hmac is set equal to the hmac secret we used to encrypt that token:

JWT.decode(token, hmac, "H256")

=> [
     {"user_id"=>"1"},
     {"typ"=>"JWT", "alg"=>"HS256"}
]

As we've seen here, it's not too complicated to implement JWT in Ruby. So, let's get started on implementing it in Rails.

Set Up

This post assumes that you have a Rails 5 API app up and running, and that the app has a User model. We're using the bcrypt gem to hash and store user passwords, so your User model should include the has_secure_password method.

Building a JWT Auth Library

Since we'll need to regularly encode and decode JWTs, it makes sense to write a plain old Ruby class (PORO), that will wrap up some of that functionality.

We'll put this class, which we'll call Auth, inside our lib/ directory.

  • Add the jwt gem to your Gemfile and bundle install:
# Gemfile

gem 'jwt'  
  • Create a file lib/auth.rb
  • Add your lib/ directory to the Rails autoload path:
# config/application.rb

config.autoload_paths << Rails.root.join('lib')  
  • Define your Auth class to have a method, .issue, which will be responsible for generating a JWT from a given user's information, and a .decode method, which will decode a given JWT:
require 'jwt'

class Auth

  def self.issue(payload)

  end

  def self.decode(token)

  end

end  

We'll start with our .issue method.

Generating a JWT: Auth.issue

This method will simply wrap the JWT.encode method that the jwt gem makes available to us. This method takes in three arguments:

  • The data, in the form of a hash, that you will to encode in the JWT
  • The key to your hashing algorithm
  • The type of hashing algorithm

For example:

payload = {name: "sophie"}  
secret_key = "masd82348$asldfja"  
algorithm = "HS256"

JWT.encode(payload, secret_key, algorithm)  
=> "esdiva23euihrusdfcnkjz2snciusdhuihr7480y2qikjh8"

Tada!

How, though, will we generate a super secret key?

Generating a Hashing Key

We'll use the Digest module that is native to Ruby to generate our secret key. We'll generate our key in the command line, in the Pry console, and add it to our environment as an environment variable. We'll use Figaro to make sure that environment variable doesn't get pushed up to GitHub.

$ pry 
pry > Digest::SHA256.digest('beyonce')  
 => "\x85\x11\xFA\xEF\xF2A\x11\xC7\x90\x9C!{\xDC\x11W\xFB\x93\xE5\xA3\xCD\xE3\xC2\x9E#7\xC4\xCDa\xCF\xC9/\xEA"

We'll add it to our application.yml like this:

# config/application.yml
AUTH_SECRET: \x85\x11\xFA\xEF\xF2A\x11\xC7\x90\x9C!{\xDC\x11W\xFB\x93\xE5\xA3\xCD\xE3\xC2\x9E#7\xC4\xCDa\xCF\xC9/\xEA  

Okay, now we're ready to finish up that Auth.issue method.

Note: You can also generate a secret in Rails by running rake secret in the command line in the directory of your app.

Auth.issue
# lib/auth.rb  
require 'jwt'

class Auth

  ALGORITHM = 'HS256'

  def self.issue(payload)
    JWT.encode(
      payload,
      auth_secret,
      ALGORITHM)
  end

  def self.decode(token)

  end

  def self.auth_secret
    ENV["AUTH_SECRET"]
  end
end  

Notice that the hashing algorithm is stored in a class constant, ALGORITHM. This is because our .decode method will also need to access that information. Similarly, we've wrapped our accessing of the ENV["AUTH_SECRET"] in a method call, because we'll also need that data for our .decode method

While we're here, let's define that decode method.

Decoding a JWT: Auth.decode

Our Auth.decode method will simply wrap the JWT.decode method that the jwt gem makes available to us. This method takes in three arguments:

  • The JWT that we want to decode,
  • The hashing algorithm's secret key
  • The type of hashing algorithm
# lib/auth.rb  
require 'jwt'

class Auth

  ALGORITHM = 'HS256'

  def self.issue(payload)
    JWT.encode(
      payload,
      auth_secret,
      ALGORITHM)
  end

  def self.decode(token)
    JWT.decode(token, 
      auth_secret, 
      true, 
      { algorithm: ALGORITHM }).first
  end

  def self.auth_secret
    ENV["AUTH_SECRET"]
  end
end  

Okay, now we're ready to use our Auth library in our controllers.

Authorizing a User in the Sessions Controller

First, we'll need to define a route that maps to Sessions#create

# config/routes.rb

...
post '/login', to: "sessions#create"  

This is the route to which our client-side application needs to POST the user's email and password, when the user fills out the log in form.

Let's build out the create action of our Sessions Controller.

# app/controllers/sessions_controller.rb
class SessionsController < ApplicationController

  skip_before_action :authenticate

  def create
    user = User.find_by(email: auth_params[:email])
    if user.authenticate(auth_params[:password])
      jwt = Auth.issue({user: user.id})
      render json: {jwt: jwt}
    else
    end
  end

  private
    def auth_params
      params.require(:auth).permit(:email, :password)
    end

Here, we use the data in the auth_params to authorize our user via bcrypt. If the user can be authenticated, we generating our JWT, using the Auth library we just defined. Here's the line where the magic happens:

jwt = Auth.issue({user: user.id})  

Note that we are encrypting the user's ID, not an email and password. This is because the user's ID is a unique identifier without being sensitive information (like a password).

Note: my auth_params are grabbing data that is nested under the auth key. This is because my client-side application, an Ember app, is structuring data in the authorization request in that manner. You may structure the data in your requests differently, and therefore need to define slightly different strong params.

Then, we are sending the JWT back to the client-side app, as JSON, where it will be stored in localStorage.

Phew!

Defining the current_user Method

Okay, so, if our client-side app is set up properly, all subsequent requests to our API will include the following header:

"HTTP_AUTHORIZATION" => "Bearer <super encoded JWT>"

So, our current_user method, which we'll define in the Application Controller, will need to decode the JWT. Let's do it.

 # app/controllers/application_controller.rb

class ApplicationController < ActionController::API  
  before_action :authenticate 

  def logged_in?
    !!current_user
  end

  def current_user
    if auth_present?
      user = User.find(auth["user"])
      if user
        @current_user ||= user
      end
    end
  end

  def authenticate
    render json: {error: "unauthorized"}, status: 401 
      unless logged_in?
  end

  private

    def token
      request.env["HTTP_AUTHORIZATION"].scan(/Bearer 
        (.*)$/).flatten.last
    end

    def auth
      Auth.decode(token)
    end

    def auth_present?
      !!request.env.fetch("HTTP_AUTHORIZATION", 
        "").scan(/Bearer/).flatten.first
    end
end  

Some of this code should look pretty familiar: the current_user method is in charge of retrieving the current user, the logged_in? method returns true or false, depending on the presence of a current user, and the authenticate method acts as a gatekeeper, returning an error to the client-side application if there is no logged in user.

Let's dive in to our current_user method.

  • First, we check to see if our request object has a header, or a key, of ["HTTP_AUTHORIZATION"] and if the value of that header includes a realm designation of Bearer. If so...
  • Grab the token out of the value of that header, which would be something like "Bearer <encoded JWT>"

  • Then, in our #auth method, we use our Auth library to decode the token:
Auth.decode(token)  
  • Finally, we use the decoded token, which is now in the below format, and contains a user's unique ID
{user: 1}

to find and authorize the current user.

And that's it!

subscribe and never miss a post!

Blog Logo

Sophie DeBenedetto

comments powered by Disqus
comments powered by Disqus