Skip to content

Rails auth API Guide

sebastian.mihalache edited this page Mar 6, 2020 · 4 revisions

Intro

  • rails API auth solution with doorkeeper
  • check this example API - auth-example-api

Setup

  • create a Users model rails g model User email:string:uniq password_digest:string

  • add [doorkeeper](https://rubygems.org/gems/doorkeeper) and [bcrypt](https://rubygems.org/gems/bcrypt) gems and run bundle install

  • initialize doorkeeper by running bundle exec rails generate doorkeeper:install and follow the on screen instructions

    • edit the following in config/initializers/doorkeeper.rb
    • add the following:
  use_refresh_token
  access_token_expires_in 30.minutes
    • run rails generate doorkeeper:migration
    • edit the CreateDoorKeeperTables migration - remove the oauth_applications, oauth_access_grants, oauth_access_grants tables and indexes, remove the null: false from t.references :application, null: false and add the following at the end:
  add_index :oauth_access_tokens, :token, unique: true
  add_index :oauth_access_tokens, :refresh_token, unique: true
  add_foreign_key(:oauth_access_tokens, :users, column: :resource_owner_id)
    • it should look similar to this:
class CreateDoorkeeperTables < ActiveRecord::Migration[6.0]
  def change
    create_table :oauth_access_tokens do |t|
      t.references :resource_owner, index: true
      t.references :application

      t.string :token, null: false

      t.string   :refresh_token
      t.integer  :expires_in
      t.datetime :revoked_at
      t.datetime :created_at, null: false
      t.string   :scopes

      t.string   :previous_refresh_token, null: false, default: ""
    end

    add_index :oauth_access_tokens, :token, unique: true
    add_index :oauth_access_tokens, :refresh_token, unique: true
    add_foreign_key(:oauth_access_tokens, :users, column: :resource_owner_id)
  end
end
    • run rails db:migrate

Implement auth logic

  • edit app/models/user.rb - add the following
  has_secure_password

  before_save { email.downcase! }

  validates :email, presence: true, uniqueness: { case_sensitive: false }, format: { with: URI::MailTo::EMAIL_REGEXP }
  validates :password, length: { minimum: 6 }, allow_nil: true

  def tokens
    access_token = Doorkeeper::AccessToken.create!(
      resource_owner_id: id,
      expires_in: Doorkeeper.configuration.access_token_expires_in,
      use_refresh_token: Doorkeeper.configuration.refresh_token_enabled?
    )
    Doorkeeper::OAuth::TokenResponse.new(access_token).body
  end
  • create app/serializers/user_serializer.rb
  class UserSerializer < ActiveModel::Serializer
    attributes :email

    has_one :tokens, if: :with_auth_tokens?

    def with_auth_tokens?
      instance_options[:with_auth_tokens]
    end
  end
  • add api error handling - if you haven't already (see the guide from here)

  • create app/controllers/api/v1/base_controller.rb

class Api::V1::BaseController < ActionController::API
  include ApiErrorHandling

  before_action :doorkeeper_authorize!

  protected

    def doorkeeper_unauthorized_render_options(error: nil)
      {
        json: {
          code: error.state,
          message: 'Not authorized',
          expired: error.reason == :expired
        }
      }
    end

    def render_error_message(message = nil)
      render json: {
        message: 'Validation Failed',
        errors: [message],
        code: 'unprocessable_entity'
      }, status: :unprocessable_entity
    end

    def current_user
      @current_user ||= begin
        if doorkeeper_token.present?
          User.find(doorkeeper_token[:resource_owner_id])
        end
      end
    end
end
  • create app/controllers/api/v1/sessions_controller.rb
class Api::V1::SessionsController < Api::V1::BaseController
  include Api::V1::SessionsControllerDoc

  skip_before_action :doorkeeper_authorize!, only: :create

  def index
    render json: current_user
  end

  def create
    user = User.find_by(email: params[:email].downcase)

    if user&.authenticate(params[:password])
      render json: user, with_auth_tokens: true
    else
      render_error_message(I18n.t('errors.login'))
    end
  end

  def destroy
    doorkeeper_token.revoke

    head :ok
  end
end
  • create app/controllers/api/v1/users_controller.rb
class Api::V1::UsersController < Api::V1::BaseController
  include Api::V1::UsersControllerDoc

  skip_before_action :doorkeeper_authorize!, only: :create

  def create
    user = User.create!(user_params)

    render json: user, with_auth_tokens: true
  end

  private

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

add the following under the en key inside config/locales/en.yml

  errors:
    login: "Invalid email or password"
  • configure routes - add the following inside config/routes.rb
  use_doorkeeper scope: 'api/v1/sessions' do
    skip_controllers :applications, :authorizations, :authorized_applications, :token_info
  end

  namespace :api do
    namespace :v1 do
      resources :users, only: :create

      resources :sessions, only: [:create, :index] do
        delete '/', action: :destroy, on: :collection
      end
    end
  end

Test using postman

  • register: POST http://localhost:3000/api/v1/users
    • body:
{
	"user": {
		"email": "[email protected]",
		"password": "sUpErSeCuRePass"
	}
}
    • reponse:
{
    "email": "[email protected]",
    "tokens": {
        "access_token": "YjMCzHZLCwYwjzHbSYqKEt-AMki8w9e3ZOuRKMZYu7A",
        "token_type": "Bearer",
        "expires_in": 1800,
        "refresh_token": "wtHHms3LqHVmI3RnRmQIVeyC6UWhR8IEFBwiYiUKpso",
        "created_at": 1578064218
    }
}
  • login: POST http://localhost:3000/api/v1/sessions
    • body:
{
	
	"email": "[email protected]",
	"password": "sUpErSeCuRePass"
}
    • response:
{
    "email": "[email protected]",
    "tokens": {
        "access_token": "E8EmdWcnH-9eKrcU2qsY2WVpVi8Bm-9aDtM-7Hztz_Y",
        "token_type": "Bearer",
        "expires_in": 1800,
        "refresh_token": "kn73Q_Xs5naUlj5z1Nqm2t-fzfrdlw5WEo6CN93Y8jA",
        "created_at": 1578064782
    }
}
  • refresh token: POST http://localhost:3000/api/v1/sessions/token
    • body:
{
	"grant_type": "refresh_token",
	"refresh_token": "kn73Q_Xs5naUlj5z1Nqm2t-fzfrdlw5WEo6CN93Y8jA"
}
    • response:
{
    "access_token": "BFIVmi0Fgze4_KRDKeTxXL8RpaZ_-c1oNOVViMzD9lk",
    "token_type": "Bearer",
    "expires_in": 1800,
    "refresh_token": "DbhFbAksdZaolD1NL_M07XXsMqZZV-pfeu_Yrp753rI",
    "created_at": 1578065292
}
  • logout: POST http://localhost:3000/api/v1/sessions

header:

Authorization: Bearer BFIVmi0Fgze4_KRDKeTxXL8RpaZ_-c1oNOVViMzD9lk
  • unauthorized response
{
    "code": "unauthorized",
    "message": "Not authorized",
    "expired": false
}
  • Aditionally - you can check this postman collection
Clone this wiki locally