Devise and JWT in Rails
Tags: rails, authentication, jwt, devise
Published: 2020-12-05 02:09:03

I needed to implement Devise and JWT using Rails (Rails 5), and I thought, how hard could this be?  Boy was I naive...  Now there is a lot of information out there on how to do this, but each resource was using a different method and nothing really seemed to work.  Well, I've finally figured it out and I want to share it with the world for 2 reasons:
1. It may save someone days of researching and trial-and-error.
2. Selfishly, I want to know where I can go to look it up for next time

Warning, this post will assume some knowledge of Rails and a few popular gems, it's a little bit more advanced than my normal stuff so far.  So here we go.

##How does it work?

First thing's first, there are a few ways this can be handled.  There is (was?) a Devise-JWT gem that integrated JWT and worked very similarly to Devise's regular flow.  When I tried to go that route, it did not work and I wasted many, many hours troubleshooting.  I did eventually succeed in registration, but the sign_in functionality was still not working.  It's very probable that this was do to user error, but regardless, I found my way to be much simpler.

Here's how it works.
![Devise and JWT Diagram](https://dev-to-uploads.s3.amazonaws.com/i/wsmcg0zovzuqxyexu5fh.png)

So basically, you can really think about this in two steps.  Step 1 is the standard devise-driven authentication.  Step 2 is passing the JSON Web Token back and forth.

##Implementation

#### Project Generation
First, let's build our project.  Since we don't need the full Rails functionality because we'll be setting up a separate front-end, we can use the --api flag `rails new example-project --api`.  One of the effects of this flag is that the project will be set up without rails sessions - this is important.

#### Gemfile
Once we've built our project, first thing we'll do is build out the Gemfile.  For the purposes of our authentication flow, we'll need 3 gems
* `devise` for actual authentication 
* `jwt` for handling the JSON Web Tokens we'll be passing back and forth.  
* `bcrypt` for password-related unit testing - this only needs to be included in the test environment because otherwise it's included in Devise.
* BONUS: I pretty much always add `pry` to help with debugging, and it comes in real handy when I need to check what params are coming over.

#### Devise Initializer
To configure Devise, we'll run `rails generate devise:install` from our console to create an initializer file: config/initializers/devise.rb. The good news is that we can largely keep the default configuration; the only special thing we need to do is to set `config.skip_session_storage = [:http_auth]` *(about quarter way down the file)*.

#### User Model
Now we need to set up our user model.  Devise has a special way to do this by running `rails generate devise User`. This command creates a User model and prefills it with some Devise functionality, it also creates a database 'devise_create_users' migration, and adds a line to the routes file: `devise_for :users` which creates routes to the default Devise Controllers.

Once the User model is created, we can finish configuring Devise by selecting which modules we want and adding it after the `devise` macro.  For my app, I just used the basic defaults: `devise :database_authenticatable, :registerable`

One last thing before we can call the User model ready.  Since a given JSON Web Token (JWT) will be associated to a given user, it makes sense to think of a user "creating" their token.  Additionally, the goal is to get as much of the app's logic in the models, so to address both of these concerns we will place the logic of creating a JWT in the User model.  Here we use the JWT gem to encode a token containing only the user's id.  How can the id be the only thing we need you ask?  Thinking back to our "How Does It Work" Diagram above, remember that the user will need to pass in their credentials as parameters at the sign-in page and, if successful, the server will issue an encrypted token for them.  This is that token, so it will only be used to authenticate that the user is who they say they are once they've **already logged in** and they try to make a subsequent call to the API.  Thus, we only need a way to identify the user: their unique `id` attribute works perfectly for this purpose.
```
def generate_jwt
  JWT.encode({id: id, exp: 60.days.from_now.to_i}, Rails.application.secrets.secret_key_base)
end
```

#### Routes
As stated above, the `rails generate devise User` generator will create a route for us automatically that looks like this: `devise_for :users`.  For our purposes, the default controllers aren't going to work on their own because they are meant to operate via sessions, which we will not have in our api-only implementation.  So, we'll need to overwrite some of the default functionality - to do this, we need to point to custom registrations and sessions controllers:
```
devise_for :users,
controllers: {
  registrations: :registrations,
  sessions: :sessions
}
```

#### Database
Also stated above, the `rails generate devise User` generator will create our database migration for us, so the only change we need to make is uncommenting any non-default modules you added in your User model, as well as adding any custom fields you may need.  Once you're done, run `rake db:migrate` and we're done here.

## Intermission (Coffee Break)
We've gotten through a lot already, but there's quite a bit more to come, so before we get into the controllers, which contain most of our logic and functionality, take a quick breather and grab a fresh cup of coffee.  If you're following along, this is a good time to double check that everything is correct in your app so far...

Ready to continue?  Okay, let's do this!

## Controllers
There are three controllers that we're going to be concerned with for this, and each of these 3 controllers will have a specific job from the diagram at the top of this article.

1. The **Application Controller** is where we will process a JWT when a user sends a request to our API.  It's vital to keep in mind that the Application Controller is not concerned with credentials - it simply checks for a valid JWT.
2. The **Registrations Controller** is where a user will create his/her credentials, and it will assign the JWT to the user once complete.
3. The **Sessions Controller** is where a user will authenticate his/her credentials and it will assign the JWT to the user if successful.

#### Application Controller < ActionController::API
We will set up our JWT processing functionality first because, once a JWT is assigned, we'll want to check to make sure it's working correctly.  Since we know that we will be passing in JSON, we will start off the Application Controller with the following line `respond_to :json`. Since all other controllers inherit from the Application Controller, we only need to do this for this controller - it will automatically be passed down to the rest.  This is also where we'll want to provide our app with similar private methods to what the standard Devise implementation would give us, so let's set up our authentication method `authenticate_user!` as well as a `signed_in?` and `current_user` method, then we'll look at how to get them to work.

For our `authenticate_user!`, we know that we want this to reject a user as unauthorized unless they are correctly signed in.  We also know we'll eventually have a `signed_in?` method available, so let's go ahead and proceed using that:
```
def authenticate_user!(options = {})
  head :unauthorized unless signed_in?
end
```
But for this to work, of course, we need to define `signed_in?`.  Default Devise does this by checking the session for the presence of a user_id.  We won't have a session for this, but what we *will* have is a JWT.  We now know that we need a method to somehow pull a user's `id` out of the JWT and return it.  Let's call it `@current_user_id` and use that future value in our signed_in? method like so:
```
def signed_in?
  @current_user_id.present?
end
```
While we're at it, since we know that we'll have a `@current_user_id` to work with, let's use it to define our current_user method too.  We need this to take the id and search our database for a corresponding user record:
```
def current_user
  @current_user ||= super || User.find(@current_user_id)
end
```

That's easy enough, essentially just copying the Devise methods, now we just have to find a way to extract that id from a passed JWT.  One final reminder: remember that this controller is **NOT** meant to make sure that the user authenticates against his/her credentials, it's just to see whether they are signed in or not by looking at the JWT.  If a user *HAS* a valid JWT, it means that they have correctly authenticated their credentials and the server *gave* them one.  With that in mind, this is actually super simple using the `jwt` gem:
```
def process_token
  jwt_payload = JWT.decode(request.headers['Authorization'].split(' ')[1], Rails.application.secrets.secret_key_base).first
  @current_user_id = jwt_payload['id']
end
```
That will work, assuming that there IS an Auth header, and that it has a valid JWT.  I'm not willing to bet that either of these are always going to happen, so let's put some error handling around it.  We want to throw an error if an invalid JWT is sent, but not if there is no Auth header sent at all:
```
def process_token
  if request.headers['Authorization'].present?
    begin
      jwt_payload = JWT.decode(request.headers['Authorization'].split(' ')[1].remove('"'), Rails.application.secrets.secret_key_base).first
      @current_user_id = jwt_payload['id']
    rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError
      head :unauthorized
    end
  end
end
```
There!  Now there's just one last step.  We need to make sure that the token is processed before we try to take any other action.  To do this, we just need to add `before_action :process_token` underneath `respond_to :json`.  Now whenever our app is called, it will process the token (if provided) and *then* take whatever action is required.

#### Registrations Controller < Devise::RegistrationsController
Okay, next step is to provide our app the ability to register a new user and assign them a JWT to be passed to our Application Controller for processing. As long as we're just using the default attributes for Devise (and calling them "sign_up_params", we don't need to worry about whitelisting parameters because Devise is already doing it for us.  The reason we need to have our own controller is so that we can have the user instance build its token for the controller to deliver it.  On the client side, we would use this returned token to store in a `httpOnly` cookie, *(or whatever other storage option you prefer)*.
```
def create
  user = User.new(sign_up_params)

  if user.save
  token = user.generate_jwt
    render json: token.to_json
  else
    render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
  end
end
```

#### Sessions Controller < Devise::SessionsController
Finally, the last step in our implementation!  Just gotta set up the Sessions Controller so that a user can return and sign back in, and it works the same way as the Registrations Controller.  The user will submit params through the front-end, including their email, which our API will use to query the database and return our user instance.  Then we'll validate that the password they provided matches the stored password and, if successful, we will distribute a JWT:
```
def create
  user = User.find_by_email(sign_in_params[:email])
    
  if user && user.valid_password?(sign_in_params[:password])
    token = user.generate_jwt
    render json: token.to_json
  else
    render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
  end
end
```

## Wrap-Up

So there it is.  This is how I was finally able to get JWT working with server-side authentication using Devise, the de-facto standard for Rails.  Once I realized that JWT is really a separate process from authenticating credentials, it wasn't so bad to figure out.  Let me know what you think in the comments.  Is there a better way to combine these two gems?  Are there major issues with this implementation?  If you've successfully used `devise-jwt`, what is the secret??

Thanks so much for reading and hanging in there to the end!  Below this is just the final code (minus Gemfile and Initializer), in case you want to see it all in one place.

## Full Code:
```
# User.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :recoverable, :rememberable, :validatable, :lockable, :timeoutable, :trackable and :omniauthable
  devise :database_authenticatable, :registerable

  def generate_jwt
    JWT.encode({id: id, exp: 60.days.from_now.to_i}, Rails.application.secrets.secret_key_base)
  end
  
end


# Routes.rb
Rails.application.routes.draw do
  devise_for :users,
  controllers: {
    registrations: :registrations,
    sessions: :sessions
  }

  root to: "home#index"
end


# Database Schema
  create_table "users", force: :cascade do |t|
    t.string "email", default: "", null: false
    t.string "encrypted_password", default: "", null: false
    t.datetime "created_at", null: false
    t.datetime "updated_at", null: false
    t.index ["email"], name: "index_users_on_email", unique: true
  end


# ApplicationController.rb
class ApplicationController < ActionController::API
  respond_to :json
  before_action :process_token

  private

  # Check for auth headers - if present, decode or send unauthorized response (called always to allow current_user)
  def process_token
    if request.headers['Authorization'].present?
      begin
        jwt_payload = JWT.decode(request.headers['Authorization'].split(' ')[1], Rails.application.secrets.secret_key_base).first
        @current_user_id = jwt_payload['id']
      rescue JWT::ExpiredSignature, JWT::VerificationError, JWT::DecodeError
        head :unauthorized
      end
    end
  end

  # If user has not signed in, return unauthorized response (called only when auth is needed)
  def authenticate_user!(options = {})
    head :unauthorized unless signed_in?
  end

  # set Devise's current_user using decoded JWT instead of session
  def current_user
    @current_user ||= super || User.find(@current_user_id)
  end

  # check that authenticate_user has successfully returned @current_user_id (user is authenticated)
  def signed_in?
    @current_user_id.present?
  end

end


# RegistrationsController.rb
class RegistrationsController < Devise::RegistrationsController

  def create
    user = User.new(sign_up_params)

    if user.save
      token = user.generate_jwt
      render json: token.to_json
    else
      render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
    end
  end

end


# SessionsController.rb
class SessionsController < Devise::SessionsController

  def create
    user = User.find_by_email(sign_in_params[:email])
    
    if user && user.valid_password?(sign_in_params[:password])
      token = user.generate_jwt
      render json: token.to_json
    else
      render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
    end
  end

end
```