Devise Jwt You Need to Sign in or Sign Up Before Continuing

Cover image for Devise and JWT in Rails

Daniel Hintz

Devise and JWT in Rails

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

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                      

Enter fullscreen mode Exit fullscreen mode

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 }                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

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                      

Enter fullscreen mode Exit fullscreen mode

hickssinglaid.blogspot.com

Source: https://dev.to/dhintz89/devise-and-jwt-in-rails-2mlj

0 Response to "Devise Jwt You Need to Sign in or Sign Up Before Continuing"

إرسال تعليق

Iklan Atas Artikel

Iklan Tengah Artikel 1

Iklan Tengah Artikel 2

Iklan Bawah Artikel