{"id":18623608,"url":"https://github.com/codica2/auth-best-practices","last_synced_at":"2025-10-20T02:48:18.978Z","repository":{"id":98303145,"uuid":"261141077","full_name":"codica2/auth-best-practices","owner":"codica2","description":"Token-based Authentication Best Practices","archived":false,"fork":false,"pushed_at":"2020-05-04T10:16:15.000Z","size":113,"stargazers_count":7,"open_issues_count":0,"forks_count":2,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-03-25T09:01:38.052Z","etag":null,"topics":["auth","json-web-token","jwt","rails"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/codica2.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-04T10:16:01.000Z","updated_at":"2021-05-29T15:39:45.000Z","dependencies_parsed_at":null,"dependency_job_id":"d1612af3-538a-4712-a17c-c520e50eec7b","html_url":"https://github.com/codica2/auth-best-practices","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codica2%2Fauth-best-practices","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codica2%2Fauth-best-practices/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codica2%2Fauth-best-practices/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/codica2%2Fauth-best-practices/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/codica2","download_url":"https://codeload.github.com/codica2/auth-best-practices/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248335494,"owners_count":21086602,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth","json-web-token","jwt","rails"],"created_at":"2024-11-07T04:25:19.251Z","updated_at":"2025-10-20T02:48:18.965Z","avatar_url":"https://github.com/codica2.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Token-based Authentication Best Practices\n\n![Codica logo](images/jwt-rails-banner.jpg)\n\n## What Does a JWT Token Contain?\n\nThe token is separated into three base-64 encoded, dot-separated values. Each value represents a different type of data:\n\n### Header\nConsists of the type of the token (JWT) and the type of encryption algorithm (HS256) encoded in base-64.\n\n### Payload\nThe payload contains information about the user and his or her role. For example, the payload of the token can contain the e-mail and the password.\n\n### Signature\nSignature is a unique key that identifies the service which creates the header. In this case, the signature of the token will be a base-64 encoded version of the Rails application's secret key (`Rails.application.credentials.secret_key_base`). Because each application has a unique base key, this secret key serves as the token signature.\n\n### This application uses the next gems:\n\n- [bcrypt](https://github.com/codahale/bcrypt-ruby)\n- [jwt](https://github.com/jwt/ruby-jwt)\n- [simple_command](https://github.com/nebulab/simple_command)\n\n## Setting up a Token-based Authentication\n\n### Generate User model\n\n```ruby\nrails g model User name email password_digest\n```\n\n### Install `bcrypt` gem \n\nThe method `has_secure_password` must be added to the model to make sure the password is properly encrypted into the database: `has_secure_password` is part of the `bcrypt` gem, so we have to install it first. Add it to the gemfile:\n\n```ruby\n# Gemfile\n\ngem 'bcrypt', '~\u003e 3.1.7'\n```\n\n### Model preparations\n\nInclude `has_secure_password` and method `to_token_payload` into the model.\nIn the `payload` hash you can specify any meta data you want to pass into token such as `role`, `first_login?` etc \n\n```ruby\n# app/models/user.rb\n\nclass User \u003c ApplicationRecord\n\n  has_secure_password\n\n  def to_token_payload\n    {\n      id: id,\n      role: role\n    }\n  end\n\nend\n```\n\n### Encoding and Decoding JWT Tokens\n\nOnce the user model is done, the implementation of the JWT token generation can start. First, the jwt gem will make encoding and decoding of HMACSHA256 tokens available in the Rails application.\n\n```ruby\n# Gemfile\n\ngem 'jwt'\n```\n\nOnce the gem is installed, it can be accessed through the JWT global variable. Because the methods that are going to be used to require encapsulation, a singleton class is a great way of wrapping the logic and using it in other constructs.\n\n```ruby\nrequire 'jwt'\n\nclass JsonWebToken\n\n  class \u003c\u003c self\n\n    SECRET_KEY = Rails.application.credentials.secret_key_base\n\n    def encode(payload)\n      payload.reverse_merge!(meta)\n\n      JWT.encode(payload, SECRET_KEY)\n    end\n\n    def decode(token)\n      JWT.decode(token, SECRET_KEY).first\n    end\n\n    def meta\n      { exp: 7.days.from_now.to_i }\n    end\n\n  end\n\nend\n```\n\nTo make sure everything will work, the contents of the `lib` directory have to be included when the Rails application loads.\n\n```ruby\n# config/application.rb\nmodule Api\n  class Application \u003c Rails::Application\n    #.....\n    config.autoload_paths \u003c\u003c Rails.root.join('lib')\n    #.....\n  end\nend\n```\n\n\n### Authenticating Users\n\nInstead of using private controller methods, `simple_command` can be used. For more information about installation, check out the article `simple_command`.\n\n```ruby\n# Gemfile\n\ngem 'simple_command'\n```\n\nThen, the alias methods of the `simple_command` can be easily used in a class by writing `prepend SimpleCommand`. The command takes the user's e-mail and password then returns the user, if the credentials match. Here is how this can be done:\n\n```ruby\n# app/auth/authenticate_user.rb\nrequire 'json_web_token'\n\nclass AuthenticateUser\n\n  prepend SimpleCommand\n  attr_accessor :email, :password\n\n  def initialize(email, password)\n    @email = email\n    @password = password\n  end\n\n  def call\n    return unless user\n\n    JsonWebToken.encode(user_id: user.id, aud: user.role)\n  end\n\n  private\n\n  def user\n    current_user = User.find_by(email: email)\n\n    return current_user if current_user \u0026\u0026 current_user.authenticate(password)\n\n    errors.add(:user_authentication, 'Invalid credentials')\n  end\nend\n```\n\n### Checking User Authorization\n\nThe token creation is done, but there is no way to check if a token that's been appended to a request is valid. The command for authorization has to take the `headers` of the request and decode the token using the `decode` method in the `JsonWebToken` singleton.\n\n```ruby\n# app/auth/authorize_api_request.rb\nclass AuthorizeApiRequest\n\n  prepend SimpleCommand\n\n  def initialize(headers = {})\n    @headers = headers\n  end\n\n  def call\n    @user ||= User.find(decoded_auth_token[:user_id]) if decoded_auth_token\n    @user || errors.add(:token, 'Invalid token')\n  end\n\n  private\n\n  attr_reader :headers\n\n  def decoded_auth_token\n    @decoded_auth_token ||= JsonWebToken.decode(http_auth_header)\n  end\n\n  def http_auth_header\n    return headers['Authorization'].split(' ').last if headers['Authorization'].present?\n\n    errors.add(:token, 'Missing token')\n  end\n\nend\n\n```\n\n### Authorizing Requests\n\nTo put the token to use, there must be a `current_user` method that will 'persist' the user. In order to have `current_user` available to all controllers, it has to be declared in the `ApiController`:\n\n```ruby\nmodule Api\n\n  module V1\n\n    class ApiController \u003c ActionController::API\n\n      before_action :authenticate_request\n\n      attr_reader   :current_user\n\n      private\n\n      def token\n        JsonWebToken.decode(request.headers['Authorization'])\n      end\n\n      def user\n        User.find(token[:user_id])\n      end\n\n      def authenticate_request\n        @current_user = AuthorizeApiRequest.call(request.headers).result\n        return if @current_user\n\n        json_responce({ errors: 'Not Authorized' }, :unauthorized)\n      end\n\n    end\n  end\nend  \n```\n\n### Implementing Helper Methods into the Controllers\n\nLogin Users\n\n```ruby\n\nmodule Api\n\n  module V1\n\n    class AuthenticationController \u003c ApiController\n\n      skip_before_action :authenticate_request, only: :login\n\n      def login\n        authenticate params[:email], params[:password]\n      end\n\n      private\n\n      def authenticate(email, password)\n        command = AuthenticateUser.call(email, password)\n\n        if command.success?\n          render json: {\n            access_token: command.result,\n            message: 'Login Successful'\n          }\n        else\n          render json: { error: command.errors }, status: :unauthorized\n        end\n      end\n\n    end\n\n  end\n\nend\n\n```\n\nThe `authenticate` action will take the JSON parameters for email and password through the `params` hash and pass them to the `AuthenticateUser` command. If the command succeeds, it will send the JWT token back to the user.\n\n```ruby\n# config/routes.rb\nscope :auth do\n  post '/login', to: 'authentication#login'\nend\n```\n\n### Testing via Rspec\n\nTo check the token authentication in work we should create `rspec` test for `AuthenticationController`. Here is an example of testing `login` action\n\n```ruby\n\nresource 'Authentication' do\n\n  let!(:user) { create :user }\n\n  before do\n    header 'Accept',       'application/json'\n    header 'Content-Type', 'application/json; charset=utf-8'\n  end\n\n  post '/api/v1/auth/login' do\n    parameter :email,             'User email'\n    parameter :password,          'User password'\n\n    context '200' do\n      let(:email)  { user.email }\n      let(:password) { user.password }\n\n      let(:raw_post) { params.to_json }\n\n      example_request 'Login user' do\n        expect(status).to eq(200)\n      end\n    end\n\n    context '401' do\n      let(:email) { user.email }\n      let(:password) { 'wrongpass' }\n\n      let(:raw_post) { params.to_json }\n\n      example_request 'Failed user login' do\n        expect(status).to eq(401)\n        expect(JSON.parse(response_body)).to eq(\n          'error' =\u003e {\n            'user_authentication' =\u003e 'Invalid credentials'\n          }\n        )\n      end\n    end\n  end\nend\n\n```\n\nTo check other actions that required the user authentication you can use the following code:\n\n\n```ruby\nresource 'Users' do\n\n  let!(:user) { create :user }\n\n  let!(:id) { user.id }\n\n  before do\n    header 'Accept',       'application/json'\n    header 'Content-Type', 'application/json; charset=utf-8'\n  end\n\n  put '/api/v1/users/:id' do\n\n    with_options scope: :user do\n      parameter :email\n      parameter :first_name\n      parameter :last_name\n    end\n\n    let(:first_name) { Faker::Name.first_name }\n    let(:last_name) { Faker::Name.last_name }\n\n    context '200' do\n\n      before do \n        header 'Authorization', \"Bearer #{JsonWebToken.encode({ user_id: user.id, aud: user.role})}\"\n      end\n\n      let(:raw_post) { params.to_json }\n\n      example_request 'User updates itself' do\n        expect(status).to eq(200)\n      end\n    end\n  end\nend\n```\n\nYou need to pass user data into `JsonWebToken.encode` method to generate `Authorization Bearer` token in `before` block in your test. \n\n\n## License  \nTimebot is Copyright © 2015-2020 Codica. It is released under the [MIT License](https://opensource.org/licenses/MIT).  \n  \n## About Codica  \n  \n[![Codica logo](https://www.codica.com/assets/images/logo/logo.svg)](https://www.codica.com)\n\nThe names and logos for Codica are trademarks of Codica.\n  \nWe love open source software! See [our other projects](https://github.com/codica2) or [hire us](https://www.codica.com/) to design, develop, and grow your product.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodica2%2Fauth-best-practices","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcodica2%2Fauth-best-practices","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcodica2%2Fauth-best-practices/lists"}