Outline

For our project, our users will need the ability to create an account and log in to our application. Devise is an excellent authentication system made for Rails that allows us to easily drop-in User functionality into our project. We'll have to make some changes to our controllers to authenticate with JWT's since Devise uses session authentication by default.

Creating the User Model

In order to get started with Devise, we'll need to generate the configuration file which will contain our settings for Devise.

Generate the Devise initializer

In the root of your Rails project, run:

 rails generate devise:install

This command generates a file in config/initializers/devise.rb which will configure devise when Rails boots.

Generate the User model using Devise

In the root of your Rails project, run:

rails generate devise User

For our user profiles, we'll also need a few extra fields in addition to the ones Devise generates by default to store usernames, image URLs and user bio's. Let's go ahead and create a migration to add those columns to our Users table.

Add a username, image and bio column to the Users table

In the root of your Rails project, run:

rails g migration AddProfileFieldsToUsers username:string:uniq image:string bio:text

This migration creates a username, image and bio fields for our User. Providing the uniq option to username creates a unique index for that column.

We should now have all the migrations necessary for creating our users table. Let's go ahead and run these migrations to apply them to our database.

Run the migrations to create the users table

In the root of your Rails project, run:

rake db:migrate

Once the migration is finished, we should have our Users table created in our database and a User model generated in app/models/user.rb.

According to our specification, we want all our routes to start with /api. To achieve this, we can use a scope in our router to prefix our routes, and pass additonal options to them. We'll want to pass defaults: { format: :json } to our scope so that our controllers know that requests will be JSON requests by default.

Put the Devise routes under a scoped named api

In config/routes.rb, wrap the devise routes in a scope :api block, and pass in the options defaults: { format: :json } to the scope method

  scope :api, defaults: { format: :json } do
    devise_for :users
  end

Setting up Registration and Login

In order to authenticate Users with JWT tokens, we'll need to create a method on our User model that will generate a token for an instance of a user.

Create a method on the User model to generate JWT tokens

Create the following function in app/models/user.rb

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

In our JWT payload, we're including the id of the user, and setting the expiration time of the token to 60 days in the future. We're using the secret_key_base that rails usually uses for cookies to sign our JWT tokens (we won't be using cookies on our server) you can choose to create a different secret if you'd like. In production this key will be set using an environment variable.

While we're in app/models/user.rb, let's add a validation for usernames. We want our usernames to be required and unique (so that no two users can haave the same username), along with only allowing letters and numbers.

Add a validation to the User model for usernames

In app/models/user.rb add the following:

  devise :database_authenticatable, :registerable,
    :recoverable, :rememberable, :trackable, :validatable

+ validates :username, uniqueness: { case_sensitive: false }, presence: true, allow_blank: false, format: { with: /\A[a-zA-Z0-9]+\z/ }

Now, let's go ahead and setup our views for rendering JSON responses. We'll be using Jbuilder which is a gem that ships with Rails that allows us to create reusable JSON templates as our view layer.

Create a folder in app/views called users
Create a Jbuilder partial for user information

Create the following partial in app/views/users called _user.json.jbuilder:

json.(user, :id, :email, :username, :bio, :image)
json.token user.generate_jwt

This JSON partial will be used by our controllers whenever we're dealing with authentication. It contains the user's JWT token which is considered sensitive, so it will be the partial only ever gets rendered for the current user. Later down the road, we'll create a partial for user profiles which will be public-facing. In order for Devise to use the jbuilder template we just created, we'll need to create a couple jbuilder views in Devise's template folders. We're only going to be using two of Devise's endpoints in our API, registration and logging in.

Creating folders for devise views

Create a folder in app/views named devise.

Then, in the newly created app/views/devise, create two more folders named registrations and sessions.

Creating views for sessions and registration

Create the following template in the registrations and sessions folder made in the previous step and name them create.json.jbuilder

json.user do |json|
  json.partial! 'users/user', user: current_user
end

Next, let's override the create method on Devise's SessionController in order to customize the response behavior of the login endpoint. By default, Devise responds with a 401 status code when authentication fails, and formats the error JSON as {error: 'Invalid username or password'}. We want our login endpoint to respond with a 422 status code, and the body to follow the errors format in the documentation.

Override Devise's login behavior to respond with a 422 status code when a sign-in request fails

Create the following file in app/controllers/sessions_controller.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])
      @current_user = user
    else
      render json: { errors: { 'email or password' => ['is invalid'] } }, status: :unprocessable_entity
    end
  end
end

This create method is very simlar to the stock Devise method. It looks up our user and attempts to authenticate them but responds with a 422 when authentication fails.

Now we need to let Devise know that we want to use our own SessionController. We also need to make our login route /api/users/login instead of the default /api/users/sign_in.

Update the Devise routes in config/routes.rb

In config/routes.rb, make sure your update your devise_for routes with the following:

  scope :api, defaults: { format: :json } do
    devise_for :users, controllers: { sessions: :sessions },
                       path_names: { sign_in: :login }
  end

The controllers: { sessions: :sessions } part tells the router to use our custom SessionsController, and path_names: { sign_in: :login } replaces sign_in in our URL for our authentication endpoint with login.

By default, Devise doesn't allow addition fields beyond email and password. We'll need to add some code in app/controllers/application_controller.rb to allow our users to provide usernames on registration. The Devise Readme has instructions on how to do this which we'll be following

Configure Devise to allow usernames on sign up

Under the private keyword in app/controllers/application_controller.rb, create the following configure_permitted_parameters function:

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.permit(:sign_up, keys: [:username])
  end

Then, add the following before_action filter after the underscore_params! filter line in application_controller.rb

  before_action :underscore_params!
+ before_action :configure_permitted_parameters, if: :devise_controller?

Private methods in our controllers make sure that we can't mistakenly use them as controller actions, which usually require something to be rendered. Next, we'll need our controllers to call this function only if they're a Devise controller.

Before we can test our authentication endpoints, we'll need to override the way Devise figures out which user is logged in. Usually devise retrieves this information from cookies, but for our API we'll need to check the Authorization header of our request for a JWT token and get the logged in user from that.

Create or own authenticate_user method for authenticating users with JWTs

Create the following private authenticate_user method in application_controller.rb

  private

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) << :username
  end

  def authenticate_user
    if request.headers['Authorization'].present?
      authenticate_or_request_with_http_token do |token|
        begin
          jwt_payload = JWT.decode(token, 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
  end

Then, add a before_action filter in application_controller.rb specifying :authenticate_user

Here we're checking the incoming request for an Authorization header. The authenticate_or_request_with_http_token is part of Rails and will grab the token from the Authorization header if it's in the format Authorization: Token jwt.token.here. This conveniently gives us just the JWT, whereas if we just looked at the Authorization token we'd need to strip out the Token part before the JWT. Next, we'll attempt to decode the token. If that fails by throwing any JWT exception, we'll rescue it and send a 401 back to the client. The decode method also throws exceptions for expired tokens. If we can successfully decode the token, we'll grab the id value from the payload, and set it to the instance variable @current_user_id for later use (all of our controllers inherit from ApplicationController so we'll be able to use this value from any of our controllers). We're also avoiding any database calls, deferring any queries for the user for when we actually need it.

The top part of your application_controller.rb (before the private keyword) should look similar to this:

class ApplicationController < ActionController::Base
  # Prevent CSRF attacks by raising an exception.
  # For APIs, you may want to use :null_session instead.
  protect_from_forgery with: :null_session

  respond_to :json

  before_action :underscore_params!
  before_action :configure_permitted_parameters, if: :devise_controller?
  before_action :authenticate_user

Note that although we're calling :authenticate_user on each request, we only inturrupt the request if the JWT token is invalid, but not if the JWT token is missing. This allows requests that don't require authentication to continue. Next, let's override the authenticate_user!, current_user and signed_in? methods from Devise in application_controller.rb

Override Devise's authenticate_user! method to use the one we created

Add the following private methods to application_controller.rb

  def authenticate_user!(options = {})
    head :unauthorized unless signed_in?
  end

  def current_user
    @current_user ||= super || User.find(@current_user_id)
  end

  def signed_in?
    @current_user_id.present?
  end

Now we can access current_user and use signed_in? throughout our application as if we were using Devise without JWT's, allowing us to access the current user and checking if a user's signed in using the same syntax. authenticate_user! can also be used the same way as a before_action filter, allowing us to reject requests the require authentication using the familiar Devise syntax.

Finally, let's create a couple endpoints for a user to update and retrieve their own information.

Create routes for retrieving and updating user information

Add a :user resource to config/routes.rb

  scope :api, defaults: { format: :json } do
    devise_for :users, controllers: { sessions: :sessions },
                       path_names: { sign_in: :login }

    resource :user, only: [:show, :update]
  end

By default rails creates 6 routes for singular resources. We only need to get and update users, so we're passing only: [:show, :update] to the resource. This makes it so that only a GET and PUT/PATCH route is made.

Create a controller for updating and retrieving User information

Create app/controllers/users_controller.rb with the following code:

class UsersController < ApplicationController
  before_action :authenticate_user!
end

The before_action :authenticate_user! callback ensures that a user needs to be authenticated before the controller action is reached.

Then, create a private method on UsersController for retrieving whitelisted user params

  private

  def user_params
    params.require(:user).permit(:username, :email, :password, :bio, :image)
  end

This is the strong parameters syntax introduced in Rails 4. Only values we specified within permit will be available when we call user_params, any values in params not listed in permit will be dropped. This prevents preventing mass-assignment so that clients can't set arbitrary fields on models. Additionally, any requests without a user in the request will result in a 400 status code.

Create the show and update actions for UsersController

In app/controllers/users_controller.rb add the following two methods:

+ def show
+ end
+
+ def update
+   if current_user.update_attributes(user_params)
+     render :show
+   else
+     render json: { errors: current_user.errors }, status: :unprocessable_entity
+   end
+ end

  private

  def user_params

Both the show and update actions will render the same user JSON, so we can call render :show in our update action to reuse the same template from show that we'll be creating next. The show action doesn't require any logic since we can access current_user from our templates.

Now that our controller actions have been created, we need to create views so that they'll actually respond with JSON data.

Create the User JSON template

In app/views/users, create show.json.jbuilder with the following code:

json.user do |json|
  json.partial! 'users/user', user: current_user
end

This template uses the user partial we created earlier in app/views/users/_user.json.jbuilder is used by both our show and update actions in UsersController

Testing Authentication with Postman

Now we can to start our Rails server using rails s. In order to test the functionality and results of the endpoints we've made, we'll be using Postman to make requests to our Rails server. We've created a Postman collection for all the requests in this project for you to download.

Install Postman and import the Conduit Postman Collection

You can download Postman from getpostman.com. Postman can be installed as a native application on Mac or as a browser extension on Chrome.

We should be able to run all of the requests in the Auth folder of the Postman. The Login and Register requests in Postman should return a JWT token in the response body. In order to identify the Postman client as a user, we'll have to set the Authorization header for each request. Luckily, Postman has Environment Variables which can be shared between requests. For requests that require authentication, we've already prefilled the Authorization header to read from an environment variable named token, so we can make requests on behalf of a user by setting the token environment variable to a user's JWT token. After registering a user, you should be able to log in with the same credentials and update the fields of that user. You can customize the parameters being sent by Postman in the Body tab of each request.

Test out authentication functionality using Postman

You should be able to:

  • Create an account using the Register request in Postman
  • Test the Login endpoint using Postman
  • Try registering another user with the same email or username, you should get an error back from the backend
  • Test the Current User endpoint using Postman
  • Try logging in to the user you created with an invalid password, you should get an error back from the backend
  • Try updating the email, username, bio, or image for the user
 

I finished! On to the next chapter