Giter Site home page Giter Site logo

jwt-auth-rails-london-web-051319's Introduction

JWT Auth in Redux and Rails

This is a sample application and walks through one possible auth implementation. It does not cover everything there is to know about auth and is intended as an introduction. Please do not blindly copy/paste the code here. Use this as a guide for setting up auth in a React/Redux application using JSON Web Tokens.


RAILS BCrypt, JWT 🔐

Building Our Server

  • This section will walk through building a rails server. If you have questions about Cors, ActiveModel::Serializer, Postgres, namespacing and versioning our API, and/or general questions about Rails as an api only, refer to this guide.

  • Let's create our app with rails new backend_project_name --api --database=postgresql

  • We're going to need a few gems in our Gemfile so let's go ahead and add them: bundle add jwt && bundle add active_model_serializers && bundle add faker––if you get a gem not found error, try running gem install on each of these, or manually add them to your Gemfile.

  • Don't forget to uncomment rack-cors and bcrypt from your Gemfile.

  • Call bundle install. Your Gemfile should look something like this:

source 'https://rubygems.org'
git_source(:github) { |repo| "https://github.com/#{repo}.git" }

ruby '2.5.1'

# Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
gem 'rails', '~> 5.2.1'
# Use postgresql as the database for Active Record
gem 'pg', '>= 0.18', '< 2.0'
# Use Puma as the app server
gem 'puma', '~> 3.11'
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
# gem 'jbuilder', '~> 2.5'
# Use Redis adapter to run Action Cable in production
# gem 'redis', '~> 4.0'
# Use ActiveModel has_secure_password
gem 'bcrypt', '~> 3.1.7'

# Use ActiveStorage variant
# gem 'mini_magick', '~> 4.8'

# Use Capistrano for deployment
# gem 'capistrano-rails', group: :development

# Reduces boot times through caching; required in config/boot.rb
gem 'bootsnap', '>= 1.1.0', require: false

# Use Rack CORS for handling Cross-Origin Resource Sharing (CORS), making cross-origin AJAX possible
gem 'rack-cors'


group :development, :test do
  # Call 'byebug' anywhere in the code to stop execution and get a debugger console
  gem 'byebug', platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
  gem 'listen', '>= 3.0.5', '< 3.2'
  # Spring speeds up development by keeping your application running in the background. Read more: https://github.com/rails/spring
  gem 'spring'
  gem 'spring-watcher-listen', '~> 2.0.0'
end


# Windows does not include zoneinfo files, so bundle the tzinfo-data gem
gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby]

gem "jwt", "~> 2.1"

gem "active_model_serializers", "~> 0.10.7"

gem "faker", "~> 1.9"
  • Don't forget to enable CORS in your app. Uncomment the following in config/initializers/cors.rb. Don't forget to change the origins from example.com to *
  • Depending on the use-case and needs of our API, we might want to limit access to our app. For example, if our React frontend is deployed to myCoolReactApp.com, we might want to limit access to that domain only. If certain endpoints are meant to be public, we can make those available but limit to GET requests, for example.
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins '*'

    resource '*',
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end
  • You can refer to the rack-cors gem for more information about this file.
  • Please don't forget to change these settings before deploying your app to the internet. Please

Creating Users

class User < ApplicationRecord
  has_secure_password
end
  • You might also want to add some validations to your users:
class User < ApplicationRecord
  has_secure_password
  validates :username, uniqueness: { case_sensitive: false }
end

Quick BCrypt Tangent

  • Recall that BCrypt allows us to salt users' plaintext passwords before running them through a hashing function. A hashing function is, basically, a one way function. Similar to putting something in a meat grinder: we cannot feasibly reconstruct something that's been ground up by a meat grinder. We then store these passwords that have been 'digested' by BCrypt in our database. Never ever ever store your users' plaintext passwords in your database. It's bad form and should be avoided at all costs.

  • Let's take a look at some of the functionality provided by BCrypt:

# in rails console
> BCrypt::Password.create('P@ssw0rd')
 => "$2a$10$D0iXNNy/5r2YC5GC4ArGB.dNL6IpUzxH3WjCewb3FM8ciwsHBt0cq"
# in rails console
> salted_pw = BCrypt::Password.create('P@ssw0rd')
  => "$2a$10$YQvJPemUzm8IdCCaHxiOOes6HMEHda/.Hl60cUoYb4X4fncgT8ubG"

> salted_pw.class
  => BCrypt::Password

> salted_pw == 'P@ssw0rd'
  => true
  • BCrypt also provides a method that will take a stringified password_digest and turn it into an instance of BCrypt::Password, allowing us to call the over-written == method.
# in rails console
> sample_digest = User.last.password_digest
  => "$2a$10$SJiIJnmQJ/A4z4fFG5EuE.aOoCjacFuQMVpVzQnhPSJKYLFCoqmWy"

> sample_digest.class
  => String

> sample_digest == 'P@ssword'
 => false

> bcrypt_sample_digest = BCrypt::Password.new(sample_digest)
  => "$2a$10$dw4sYcbLXc8XRX6YGc7ve.ot6LbYevMbSpFQZUaa8tm5NI8cxBPwa"

> bcrypt_sample_digest.class
  => BCrypt::Password

> bcrypt_sample_digest == 'P@ssw0rd'
  => true

mind blown

  • We have no way of storing instances of BCrypt::Password in our database. Instead, we're storing users' password digests as strings. If we were to build our own User#authenticate method using BCrypt, it might look something like this:
class User < ApplicationRecord
  attr_accessor :password

  def authenticate(plaintext_password)
    if BCrypt::Password.new(self.password_digest) == plaintext_password
      self
    else
      false
    end
  end
end
# in rails console
> User.last.authenticate('not my password')
  => false

> User.last.authenticate('P@ssw0rd')
  => #<User id: 21, username: "sylviawoods", password_digest: "$2a$10$dw4sYcbLXc8XRX6YGc7ve.ot6LbYevMbSpFQZUaa8tm...", avatar: nil, created_at: "2018-08-31 02:11:15", updated_at: "2018-08-31 02:11:15", bio: "'Sylvia Woods was an American restaurateur who founded the sould food restaurant Sylvia's in Harlem on Lenox Avenue, New York City in 1962. She published two cookbooks and was an important figure in the community.">
class User < ApplicationRecord
  has_secure_password
end

salt bae

End of BCrypt Tangent


class Api::V1::UsersController < ApplicationController
  def create
    @user = User.create(user_params)
    if @user.valid?
      render json: { user: UserSerializer.new(@user) }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :not_acceptable
    end
  end

  private
  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
class UserSerializer < ActiveModel::Serializer
  attributes :username, :avatar, :bio
end

Rails.application.routes.draw do
  namespace :api do
    namespace :v1 do
      resources :users, only: [:create]
      post '/login', to: 'auth#create'
      get '/profile', to: 'users#profile'
    end
  end
end

  • Take some time to test this either in Postman or with JavaScript fetch:
fetch('http://localhost:3000/api/v1/users', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    Accept: 'application/json'
  },
  body: JSON.stringify({
    user: {
      username: "sylviawoods",
      password: "whatscooking",
      bio: "Sylvia Woods was an American restaurateur who founded the sould food restaurant Sylvia's in Harlem on Lenox Avenue, New York City in 1962. She published two cookbooks and was an important figure in the community.",
      avatar: "https://upload.wikimedia.org/wikipedia/commons/4/49/Syvia_of_Sylvia%27s_reaturant_N.Y.C_%28cropped%29.jpg"
    }
  })
})
  .then(r => r.json())
  .then(console.log)

Note: if you're using Postman and your formatting is set to "raw and JSON", remember to use double quotes ("") in both keys and values in the request.


Make Sure You Can POST and Create a New User Before Proceeding

intermission


JSON Web Tokens (JWT)

  • Token-based authentication is stateless. We are not storing any information about a logged in user on the server (which also means we don't need a model or table for our user sessions). No stored information means our application can scale and add more machines as necessary without worrying about where a user is logged in. Instead, the client (browser) stores a token and sends that token along with every authenticated request. Instead of storing a plaintext username, or user_id, we can encode user data with JSON Web Tokens (JWT) and store that encoded token client-side.

JWT Auth Flow:

  • Here is the JWT authentication flow for logging in:
    1. An already existing user requests access with their username and password
    2. The app validates these credentials
    3. The app gives a signed token to the client
    4. The client stores the token and presents it with every request. This token is effectively the user's access pass––it proves to our server that they are who they claim to be.
  • JWTs are composed of three strings separated by periods:

    aaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccc
    
    • The first part (aaaaaaaaaaaa) is the header

    • The second part (bbbbbbbbbbbb) is the payload - the good stuff, like who this person is, and their id in our database.

    • The third part (ccccccccccccc) is the signature. The signature is a hash of the header and the payload. It is hashed with a secret key, that we will provide (and should store in an environment variable using a gem like Figaro)

    • Head on over to jwt.io and see for yourself:

    JWTs

Encoding and Decoding JWTs

  • Since we've already added gem jwt to our gemfile, let's explore some JWT methods by opening a rails console
    • JWT.encode takes up to three arguments: a payload to encode, an application secret of the user's choice, and an optional third that can be used to specify the hashing algorithm used. Typically, we don't need to show the third. This method returns a JWT as a string.
    • JWT.decode takes three arguments as well: a JWT as a string, an application secret, and––optionally––a hashing algorithm.
#in rails console
>  payload = { beef: 'steak' }

> jwt = JWT.encode(payload, 'boeuf')
=> "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"

> decoded_hash = JWT.decode(jwt, 'boeuf')
=> [{"beef"=>"steak"}, {"alg"=>"HS256"}]

> data = decoded_hash[0]
=> {"beef"=>"steak"}

Building this functionality into our ApplicationController:

class ApplicationController < ActionController::API
  def encode_token(payload)
    # payload => { beef: 'steak' }
    JWT.encode(payload, 'my_s3cr3t')
    # jwt string: "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"
  end

  def decoded_token(token)
    # token => "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"

    JWT.decode(token, 'my_s3cr3t')[0]
    # JWT.decode => [{ "beef"=>"steak" }, { "alg"=>"HS256" }]
    # [0] gives us the payload { "beef"=>"steak" }
  end
end

  • According to the JWT Documentation: Whenever the user wants to access a protected route or resource, the user agent (browser in our case) should send the JWT, typically in the Authorization header using the Bearer schema. The content of the header should look like the following:

    Authorization: Bearer <token>


  • The corresponding fetch request might look like this:
fetch('http://localhost:3000/api/v1/profile', {
  method: 'GET',
  headers: {
    Authorization: `Bearer <token>`
  }
})

  • Knowing this, we can set up our server to anticipate a JWT sent along in request headers, instead of passing the token directly to ApplicationController#decoded_token:
class ApplicationController < ActionController::API
  def encode_token(payload)
    # payload => { beef: 'steak' }
    JWT.encode(payload, 'my_s3cr3t')
    # jwt string: "eyJhbGciOiJIUzI1NiJ9.eyJiZWVmIjoic3RlYWsifQ._IBTHTLGX35ZJWTCcY30tLmwU9arwdpNVxtVU0NpAuI"
  end

  def auth_header
    # { 'Authorization': 'Bearer <token>' }
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      # headers: { 'Authorization': 'Bearer <token>' }
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
        # JWT.decode => [{ "beef"=>"steak" }, { "alg"=>"HS256" }]
      rescue JWT::DecodeError
        nil
      end
    end
  end

  • A few things to note about the code above:
    • The Begin/Rescue syntax allows us to rescue out of an exception in Ruby. Let's see an example in a rails console. In the event our server receives and attempts to decode an invalid token:
# in rails console
> invalid_token = "nnnnnnnooooooootttttt.vvvvvvaaaallliiiiidddddd.jjjjjjjwwwwwttttttt"

> JWT.decode(invalid_token, 'my_s3cr3t', true, algorithm: 'HS256')

Traceback (most recent call last):
        1: from (irb):6
JWT::DecodeError (Invalid segment encoding)
  • In other words, if our server receives a bad token, this will raise an exception causing a 500 Internal Server Error. We can account for this by rescuing out of this exception:
# in rails console
> invalid_token = "nnnnnnnooooooootttttt.vvvvvvaaaallliiiiidddddd.jjjjjjjwwwwwttttttt"

> begin JWT.decode(invalid_token, 'my_s3cr3t', true, algorithm: 'HS256')
  rescue JWT::DecodeError
    nil
>  end
 => nil
  • Instead of crashing our server, we simply return nil and keep trucking along.

keep trucking


  • We can then complete our ApplicationController by automatically obtaining the user whenever an authorization header is present:
class ApplicationController < ActionController::API

  def encode_token(payload)
    # don't forget to hide your secret in an environment variable
    JWT.encode(payload, 'my_s3cr3t')
  end

  def auth_header
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
      rescue JWT::DecodeError
        nil
      end
    end
  end

  def current_user
    if decoded_token
      # decoded_token=> [{"user_id"=>2}, {"alg"=>"HS256"}]
      # or nil if we can't decode the token
      user_id = decoded_token[0]['user_id']
      @user = User.find_by(id: user_id)
    end
  end

  def logged_in?
    !!current_user
  end
end
  • Recall that a Ruby object/instance is 'truthy': !!user_instance #=> true and nil is 'falsey': !!nil #=> false. Therefore logged_in? will just return a boolean depending on what our current_user method returns.

  • Finally, let's lock down our application to prevent unauthorized access:
class ApplicationController < ActionController::API
  before_action :authorized

  def encode_token(payload)
    # should store secret in env variable
    JWT.encode(payload, 'my_s3cr3t')
  end

  def auth_header
    # { Authorization: 'Bearer <token>' }
    request.headers['Authorization']
  end

  def decoded_token
    if auth_header
      token = auth_header.split(' ')[1]
      # header: { 'Authorization': 'Bearer <token>' }
      begin
        JWT.decode(token, 'my_s3cr3t', true, algorithm: 'HS256')
      rescue JWT::DecodeError
        nil
      end
    end
  end

  def current_user
    if decoded_token
      user_id = decoded_token[0]['user_id']
      @user = User.find_by(id: user_id)
    end
  end

  def logged_in?
    !!current_user
  end

  def authorized
    render json: { message: 'Please log in' }, status: :unauthorized unless logged_in?
  end
end
  • A few things to note about the code above:
    • before_action :authorized will call the authorized method before anything else happens in our app. This will effectively lock down the entire application. Next we'll augment our UsersController and build our AuthController to allow signup/login.

Updating the UsersController

  • Let's update the UsersController so that it issues a token when users register for our app:
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def create
    @user = User.create(user_params)
    if @user.valid?
      @token = encode_token(user_id: @user.id)
      render json: { user: UserSerializer.new(@user), jwt: @token }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :not_acceptable
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]
end
  • It wouldn't make sense to ask our users to be logged in before they create an account. This circular logic will make it impossible for users to authenticate into the app. How can a user create an account if our app asks them to be logged in or authorized to do so? Skipping the before action 'unlocks' this portion of our app.

omg

  • Try creating a new user again with either postman or fetch and confirm that your server successfully issues a token on signup.

sign me up gif


Implementing Login

  • A token should be issued in two different controller actions: UsersController#create and AuthController#create. Think about what these methods are responsible for––a user signing up for our app for the first time and an already existing user logging back in. In both cases, our server needs to issue a new token🥇.

  • We'll need to create a new controller to handle login: rails g controller api/v1/auth. Next, let's add the following to this newly created AuthController:

class Api::V1::AuthController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def create
    @user = User.find_by(username: user_login_params[:username])
    #User#authenticate comes from BCrypt
    if @user && @user.authenticate(user_login_params[:password])
      # encode token comes from ApplicationController
      token = encode_token({ user_id: @user.id })
      render json: { user: UserSerializer.new(@user), jwt: token }, status: :accepted
    else
      render json: { message: 'Invalid username or password' }, status: :unauthorized
    end
  end

  private

  def user_login_params
    # params { user: {username: 'Chandler Bing', password: 'hi' } }
    params.require(:user).permit(:username, :password)
  end
end
  • We can simply call our ApplicationController#encode_token method, passing the found user's ID in a payload. The newly created JWT can then be passed back along with the user's data. The user data can be stored in our application's state, e.g., React or Redux, while the token can be stored client-side.

  • A few things to keep in mind about the code above:

    • User.find_by({ name: 'Chandler Bing' }) will either return a user instance if that user can be found OR it will return nil if that user is not found.
    • In the event that the user is not found, user = User.find_by(username: params[:username]) will evaluate to nil.
    • Can we call .authenticate on nil? NO!! NoMethodError (undefined method 'authenticate' for nil:NilClass)
    • Ruby, however, is lazy. If Ruby encounters &&, both statements in the expression must evaluate to true. If the statement on the left side evaluates to false, Ruby will not even look at the statement on the right. Let's see an example:
# in irb or a rails console
> true && true
  => true

> true && false
  => false


> true && not_a_variable
  NameError (undefined local variable or method `not_a_variable` for main:Object)

> false && not_a_variable
  => false
  • Let's take another look at our previous example:
@user = User.find_by(username: params[:username])
if @user && @user.authenticate(params[:password])
end
  • If @user is nil, which is falsey, ruby will not even attempt to call @user.authenticate. Without this catch, we'd get a NoMethodError (undefined method 'authenticate' for nil:NilClass).

  • Again, the client should be sending a JWT along with every authenticated request. Refer to this diagram from scotch.io:

scotch.io article on token auth

  • A sample request might look like:
fetch('http://localhost:3000/api/v1/profile', {
  method: 'GET',
  headers: {
    Authorization: `Bearer <token>`
  }
})
  • So, let's update our UsersController so that an authenticated user can access their profile information:
class Api::V1::UsersController < ApplicationController
  skip_before_action :authorized, only: [:create]

  def profile
    render json: { user: UserSerializer.new(current_user) }, status: :accepted
  end

  def create
    @user = User.create(user_params)
    if @user.valid?
      @token = encode_token({ user_id: @user.id })
      render json: { user: UserSerializer.new(@user), jwt: @token }, status: :created
    else
      render json: { error: 'failed to create user' }, status: :not_acceptable
    end
  end

  private

  def user_params
    params.require(:user).permit(:username, :password, :bio, :avatar)
  end
end
  • One final note about the snippet above: ApplicationController calls authorized before any other controller methods are called. If authorization fails, our server will never call UsersController#profile and will instead:
render json: { message: 'Please log in' }, status: :unauthorized

That's It For the Server!


External Resources

jwt-auth-rails-london-web-051319's People

Contributors

dependabot[bot] avatar lizbur10 avatar sylwiavargas avatar rrcobb avatar realandrewcohn avatar bal360 avatar maxwellbenton avatar timothylevi avatar notnotdrew avatar

Watchers

James Cloos avatar  avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.