We'll only need two gems for handling all of our backend authentication logic.
Knock is similar to Devise in that it will help us to authenticate users as well as manage a user's session with a JSON Web Token (JWT). We can also prevent unauthorized users from doing things they're not supposed to.
Knock uses Bcrypt under the hood. It will encrypt the user's password to ensure that we don't store plain text passwords in our database.
-
Add this to your Gemfile and uncomment
bcrypt
, run abundle install
gem 'knock', git: 'https://github.com/nsarno/knock', branch: 'master', ref: '9214cd027422df8dc31eb67c60032fbbf8fc100b'
-
What we're missing right now is a User model, let's set that up,
password_digest
is the special field the user model needs for an encrypted passwordrails g model User email:string password_digest:string rails db:migrate
-
Add this to our User model, the
has_secure_password
allows us to set and authenticate against the bcrypt password, we're also setting up some basic model validationsclass User < ApplicationRecord has_secure_password validates :email, presence: true end
-
Add this to our
application_controller
, the include keyword in controller allows us to use the Knock methods in all of our controllers (like authenticate_user)class ApplicationController < ActionController::API include Knock::Authenticable end
-
Generate a new controller named
user_token_controller
rails g controller user_token
-
Make sure your
user_token_controller
inherits from theauth_token_controller
which is setup by knockclass UserTokenController < Knock::AuthTokenController end
-
Create a a new file
config/initializers/knock.rb
, it contains all the config we need, as we're using thesecret_key_base
you need to ensure you have a credentials file and amaster.key
, when you deploy you'll also need to ensure theRAILS_MASTER_KEY
is included as an ENV variableKnock.setup do |config| config.token_signature_algorithm = 'HS256' config.token_secret_signature_key = -> { Rails.application.credentials.secret_key_base } config.token_public_key = nil config.token_audience = nil config.token_lifetime = 1.day config.not_found_exception_class_name = 'ActiveRecord::RecordNotFound' end
-
Add this to your routes, the create action is setup by knock and we get it through the inheritance from the
auth_token_controller
post "/login", to: "user_token#create"
-
To hit this endpoint we MUST already have a created user, run this command in
rails c
User.create(email: "[email protected]", password: "password")
-
Start your server
-
Send a POST request to
http://localhost:3000/login
, add the following JSON to the body of the request{ "auth": { "email": "[email protected]", "password": "password" } }
-
You should a response body that looks like this
{ "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTM0OTYzMjUsInN1YiI6MX0.ssEkAbN_ei_XzSOb7ClWwU6Ux1Zftas3GwEkow3tY-c" }
-
You should test to see what happens if you pass a wrong email or password combination
-
Generate a new controller named
status_controller
:rails g controller status
-
Put this in the status controller,
before_action :authenticate_user
is the same as the Devisebefore_action :authenticate_user!
class StatusController < ApplicationController before_action :authenticate_user def index render status: :ok end end
-
Add this to your routes:
get "/status", to: "status#index"
-
Hit the
/login
endpoint again in postman and copy the JWT -
Inside postman setup a GET request to
http://localhost:3000/status
, and add the following to your headers:Authorization: Bearer <jwt you just copied>
-
Run the request and you should get something like this back:
200 ok
-
Mess with the Authorization header and see if you can get a 401 status code, refresh your memory on what a 401 means
-
Hit the
/status
endpoint again, we should see something like this:{ "message": "logged in", "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE1OTM0OTc1MTgsInN1YiI6MX0.QxW4bYH8XwekTXQgFWkNCwCsKXSpz1PaLlgxMNyJ2Dw" }
-
We still need an endpoint for signing up as a new user, add a
users_controller
rails g controller users
-
Add this to the controller:
class UsersController < ApplicationController def create user = User.new(user_params) if user.save render status: :created else render status: :bad_request end end private def user_params params.require(:user).permit(:email, :password) end end
-
Add this to routes:
post "/sign-up", to: "users#create"
-
Send a POST request to
http://localhost:3000/sign-up
, add the following JSON to the body of the request{ "user": { "email": "[email protected]", "password": "password" } }
-
You should a response body that looks like this
201 created
-
We now have endpoints for logging users in and signing users up, we also have endpoints that are protected unless the user has logged in
If you're keen on seeing the client side implementation check out this post.