Building a Basic API Authentication System in Ruby on Rails from Scratch

When building applications with Ruby on Rails, setting up a secure API authentication system is crucial. While there are a plethora of gems available for Rails authentication like Devise or Authlogic, creating a custom system offers greater flexibility and insight into the workings of API security. This guide will walk you through building an authentication system from scratch, using token-based authentication to secure your API endpoints.

Understanding API Authentication

API Authentication is the process of verifying the identity of a user or system trying to access your API. In a RESTful context, this involves verifying credentials (typically via tokens) to ensure that the requestor is who they claim to be. Token-based authentication is a popular approach due to its simplicity and security features. It uses a token that the client must include in every request to gain access to protected resources.

Setting Up Your Rails Environment

Before diving into the code, ensure you have a Rails environment set up. You'll need Ruby, Ruby on Rails, and a database (SQLite for simplicity) installed.

  1. Create a new Rails application:

    bash
    1rails new auth_api --api
    2
  2. Navigate to the project directory:

    bash
    1cd auth_api
    2
  3. Generate the User model:

    bash
    1rails generate model User email:string password_digest:string
    2

    This command creates a new User model with fields for email and password_digest. The password_digest field will store encrypted passwords.

  4. Run the migration:

    bash
    1rails db:migrate
    2

User Registration and Password Handling

For user authentication, we'll implement registration (sign-up) and login endpoints. The bcrypt gem will help us securely store user passwords.

  1. Add bcrypt to Gemfile:

    ruby
    1gem 'bcrypt', '~> 3.1.7'
    2
  2. Bundle install:

    bash
    1bundle install
    2
  3. Update the User model:

    ruby
    1class User < ApplicationRecord
    2 has_secure_password
    3
    4 validates :email, presence: true, uniqueness: true
    5 validates :password, length: { minimum: 6 }
    6end
    7

    The has_secure_password method adds methods for setting and authenticating against a BCrypt password. This requires the password_digest field to be present and automatically adds validations for password presence.

  4. Create UsersController:

    bash
    1rails generate controller Users
    2
  5. Setting up the registration endpoint: In users_controller.rb:

    ruby
    1class UsersController < ApplicationController
    2 def create
    3 user = User.new(user_params)
    4 if user.save
    5 render json: { status: 'User created successfully', user: user }, status: :created
    6 else
    7 render json: { errors: user.errors.full_messages }, status: :unprocessable_entity
    8 end
    9 end
    10
    11 private
    12
    13 def user_params
    14 params.require(:user).permit(:email, :password)
    15 end
    16end
    17

    This endpoint receives user registration details, creates a new user if the data is valid, and returns appropriate status messages.

  6. Add route for registration: In config/routes.rb:

    ruby
    1Rails.application.routes.draw do
    2 resources :users, only: [:create]
    3end
    4

Token Generation and Storage

To handle token-based authentication, we'll generate tokens upon successful login and store them for future verification.

  1. Generate SessionsController for handling login:

    bash
    1rails generate controller Sessions
    2
  2. Login and token generation: In sessions_controller.rb:

    ruby
    1class SessionsController < ApplicationController
    2 def create
    3 user = User.find_by(email: params[:email])
    4 if user&.authenticate(params[:password])
    5 token = encode_token(user_id: user.id)
    6 render json: { token: token }, status: :ok
    7 else
    8 render json: { error: 'Invalid email or password' }, status: :unauthorized
    9 end
    10 end
    11
    12 private
    13
    14 def encode_token(payload)
    15 JWT.encode(payload, Rails.application.secrets.secret_key_base)
    16 end
    17end
    18

    The encode_token method uses the JWT library to encode a token with the user's ID as the payload. This token is returned upon successful login.

  3. Add route for login: In config/routes.rb:

    ruby
    1Rails.application.routes.draw do
    2 post '/login', to: 'sessions#create'
    3end
    4

Protecting API Endpoints

Likely one of the most critical parts of this guide: ensuring that authorized users can access protected resources. To do so, we'll implement a custom authentication filter.

  1. Create a require_user method: In application_controller.rb:

    ruby
    1class ApplicationController < ActionController::API
    2 before_action :authorized
    3
    4 def encode_token(payload)
    5 JWT.encode(payload, Rails.application.secrets.secret_key_base)
    6 end
    7
    8 def auth_header
    9 request.headers['Authorization']
    10 end
    11
    12 def decoded_token
    13 if auth_header
    14 token = auth_header.split(' ')[1]
    15 begin
    16 JWT.decode(token, Rails.application.secrets.secret_key_base, true, algorithm: 'HS256')
    17 rescue JWT::DecodeError
    18 nil
    19 end
    20 end
    21 end
    22
    23 def current_user
    24 if decoded_token
    25 user_id = decoded_token[0]['user_id']
    26 @user = User.find_by(id: user_id)
    27 end
    28 end
    29
    30 def authorized
    31 render json: { message: 'Please log in' }, status: :unauthorized unless current_user
    32 end
    33end
    34

    This authorized method checks for the presence of a valid JWT in the request header and verifies its authenticity.

  2. Using before_action to protect actions: Any controller inheriting from ApplicationController will have its actions protected. If you have specific endpoints that need protection, ensure they are routed through this controller.

Handling Token Expiration and Renewal

Tokens should have a limited lifespan for security reasons, and we may want to periodically refresh them to limit exposure to potential misuse.

  1. Set token expiration: In sessions_controller.rb, update encode_token:

    ruby
    1def encode_token(payload)
    2 payload[:exp] = 24.hours.from_now.to_i
    3 JWT.encode(payload, Rails.application.secrets.secret_key_base)
    4end
    5

    This adds an expiry claim to the token, making it valid for 24 hours from the time of issuance.

  2. Handling expired tokens: Update application_controller.rb:

    ruby
    1def decoded_token
    2 if auth_header
    3 token = auth_header.split(' ')[1]
    4 begin
    5 JWT.decode(token, Rails.application.secrets.secret_key_base, true, algorithm: 'HS256')
    6 rescue JWT::ExpiredSignature
    7 nil
    8 end
    9 end
    10end
    11

    This method will return nil if the token has expired, prompting an unauthorized error response.

Code Review and Security Considerations

Building a custom authentication system requires attention to security. Ensure that all sensitive data, such as secret keys, are securely stored. Regularly review and update your code to address potential vulnerabilities. Implement logging to monitor suspicious activity, and consider implementing HTTPS to ensure data transmission security.

Conclusion

By following this guide, you've built a basic, custom API authentication system in Rails. This solution, while simplistic, provides a foundation on which you can build a more advanced system tailored to your needs. By avoiding third-party gems, you've gained insight into how authentication works, giving you more control and understanding over your app's security.

For further learning, consider exploring OAuth 2.0, OpenID Connect, and other advanced authentication mechanisms. Happy coding!

Suggested Articles