Skip to content

cgibsonmm/Devise_JWT_Warden

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

17 Commits
 
 
 
 

Repository files navigation

Devise_JWT_Warden

lock

Devise with a Rails API

Objectives

  • Build an easy to use full-featured option to handle User Auth with a Rails API

$ rails new devise_auth_app --api --database=postgresql

Once everything is installed move into the rails projectcd devise_auth_app

let's install the required gems

gem 'devise'
gem 'jwt'

# we also need to uncomment
gem 'rack-cors'

and now run$ bundle install


Now that we have the required gems in our app, let's run the devise installer.

$ rails g devise:install

when running this, you will get this output in the console, don't worry about any of this now.

===============================================================================

Some setup you must do manually if you haven't yet:

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root to: "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. You can copy Devise views (for customization) to your app by running:

       rails g devise:views

===============================================================================

Build out the user model

One excellent feature of Devise is that it builds a full User model for you out of the box and can also be edited if needed. In only one line!

$ rails g devise User

Open your db/migrations/ folder, and you will see the user migration that was generated by Devise.

# frozen_string_literal: true

class DeviseCreateUsers < ActiveRecord::Migration[6.0]
  def change
    create_table :users do |t|
      ## Database authenticatable
      t.string :email,              null: false, default: ""
      t.string :encrypted_password, null: false, default: ""

      ## Recoverable
      t.string   :reset_password_token
      t.datetime :reset_password_sent_at

      ## Rememberable
      t.datetime :remember_created_at

      ## Trackable
      # t.integer  :sign_in_count, default: 0, null: false
      # t.datetime :current_sign_in_at
      # t.datetime :last_sign_in_at
      # t.string   :current_sign_in_ip
      # t.string   :last_sign_in_ip

      ## Confirmable
      # t.string   :confirmation_token
      # t.datetime :confirmed_at
      # t.datetime :confirmation_sent_at
      # t.string   :unconfirmed_email # Only if using reconfirmable

      ## Lockable
      # t.integer  :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts
      # t.string   :unlock_token # Only if unlock strategy is :email or :both
      # t.datetime :locked_at


      t.timestamps null: false
    end

    add_index :users, :email,                unique: true
    add_index :users, :reset_password_token, unique: true
    # add_index :users, :confirmation_token,   unique: true
    # add_index :users, :unlock_token,         unique: true
  end
end

For now, let's leave this alone; if you want to add more values, check out the Devise documentation.

$ rails db:migrate


At this point, we need to set up our API with rack-cors, and we need to build our Devise Strategy to use Warden and JWT

./config/initializers/cors.rb

# Be sure to restart your server when you modify this file.

# Avoid CORS issues when API is called from the frontend app.
# Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests.

# Read more: https://github.com/cyu/rack-cors

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
             headers: :any,
             methods: %i[get post put patch delete options head]
  end
end

./config/initializers/devise.rb

## around line 268 ##

# ==> Warden configuration
  # If you want to use other strategies, that are not supported by Devise, or
  # change the failure app, you can configure them inside the config.warden block.
  #
  config.warden do |manager|
    # manager.intercept_401 = false
    manager.strategies.add :jwt, Devise::Strategies::JWT
    manager.default_strategies(scope: :user).unshift :jwt
  end

And at the end of the file, we can place this code.

### Taking from GoRails.com ###
 module Devise
  module Strategies
    class JWT < Base
      def valid?
        request.headers['Authorization'].present?
      end

      def authenticate!
        token = request.headers.fetch('Authorization', '').split(' ').last
        payload = JsonWebToken.decode(token)
        success! User.find(payload['sub'])
      rescue ::JWT::ExpiredSignature
        fail! 'Auth token has expired'
      rescue ::JWT::DecodeError
        fail! 'Auth token is invalid'
      end
    end
  end
end

That is all the configuration we have to handle for now. Let's just add a few controllers and API endpoints.


we can start $ rails s and make sure there are no errors

let's configure the routes that the basic API will have

`./config/routes.rb``

Rails.application.routes.draw do
  devise_for :users, controllers: { registrations: 'registrations' }
  namespace :api do
    namespace :v1 do
      get 'post/index'
      post :auth, to: 'authentication#create'
      get  '/auth' => 'authentication#fetch'
    end

    namespace :v2 do
      # Things yet to come
    end
  end
end

The line devise_for builds a bunch of devise endpoints, most of which we will not use but reference the Devise docs for more information.

And namespacing allows us to version our API without errors.

Now to configure ./app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  skip_before_action :verify_authenticity_token
end

We need to modify this class to inherit from ActionController::Base instead of ::API due to Devise and since we did that, Rails now expects all requests to come from special Rails forms the have built-in Auth tokens, so we need to skip that check app-wide.

Since we are not are using ::Base, we can make our API controller that the rest of our app can inherits from.

$ touch ./app/controllers/api_controller.rb

class ApiController < ApplicationController
  before_action :set_default_format
  before_action :authenticate_user!

  private

  def set_default_format
    request.format = :json
  end
end

This file set our default response to JSON, and I chose to lock every endpoint in our API behind the fact that the user is logged in.

We also need to build a RegistrationController that will be used to register new users to our site. I build it this way for a few workarounds for Devise.

$ touch ./app/controllers/registrations_controller.rb

class RegistrationsController < Devise::RegistrationsController
  respond_to :json

  def create
    @user = User.new(sign_up_params)
    if @user.save
      render json: @user
    else
      render json: { errors: @user.errors.full_messages }
    end
  end

  private

  def sign_up_params
    params.permit(:email, :password, :password_confirmation)
  end
end

Now we can build the controllers inside our API $ touch ./app/controllers/api/v1/post_controller.rb

class Api::V1::PostController < ApiController
  before_action :authenticate_user!

  def index
    puts current_user.email
    render json: { user: current_user.email, list: [1, 2, 3] }
  end
end

And we still need to build the JsonWebToken class we can do that in our models' folder $ touch ./app/models/json_web_token.rb

class JsonWebToken
  def self.encode(payload)
    expiration = 2.weeks.from_now.to_i
    JWT.encode payload.merge(exp: expiration), Rails.application.credentials.fetch(:secret_key_base)
  end

  def self.decode(token)
    JWT.decode(token, Rails.application.credentials.fetch(:secret_key_base)).first
  end
end

At this point, if we have the rails server running, we can pass a GET request to the API and see what happens. In Postman I sent a GET request to http://localhost:3000/api/v1/post/index

If everything is working correctly, we will get the response of

{
    "error": "You need to sign in or sign up before continuing."
}

If this works, you have everything but the last piece of the puzzle working! The only thing left would be to create a User and then sign in with them, which will return a JWT that you can then place in LocalStorage in your front-end application.


We still need to create a last controller that will handle the two routes we placed in our ./config/routes.rb file.

post :auth, to: 'authentication#create'
get  '/auth' => 'authentication#fetch'

The first will allow the user to sign in, and the second will return all of the data from the currently signed-in users. Maybe for a user profile page.

$ touch ./app/controller/api/v1/authentication_controller.rb

class Api::V1::AuthenticationController < ApiController
  skip_before_action :authenticate_user!, only: [:create]
  def create
    user = User.find_by(email: params[:email])
    puts user
    if user&.valid_password?(params[:password])
      render json: { token: JsonWebToken.encode(sub: user.id) }
    else
      render json: { errors: 'invalid' }
    end
  end

  def fetch
    render json: current_user
  end
end

Okay, That's it. Let's test everything out.

first, in Postman we can create a user POST to http://localhost:3000/users

{
  "email": "[email protected]",
  "password": "1234567"
}

this should return a new user

{
  "id": 4,
  "email": "[email protected]",
  "created_at": "2020-03-06T13:35:05.903Z",
  "updated_at": "2020-03-06T13:35:05.903Z"
}

I set this up so that once a user signs up, they will need to sign in. A great to have if you want a user to confirm their email and adds extra security.

So now, we send a POST to http://localhost:3000/api/v1/auth with the user's email and password.

{
  "email": "[email protected]",
  "password": "1234567"
}

and this will return your JWT

{
  "token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjQsImV4cCI6MTU4NDcxMTQ1M30.nbN-8A964zmM4PnDj8DuioqhSYWmBAF-Sj2Uhppim2E"
}

Postman

Save this in Local Storage and make sure to pass this token in your headers every time you make an Axios request. If there is any place you do not need a user to be signed in throw skip_before_action :authenticate_user!, only: [:action_to_skip_here]

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published