Skip to content

danielinoa/seshlock

Repository files navigation

Seshlock

Tests

Session token management for Rails applications. Seshlock provides secure access and refresh token authentication backed by Active Record.

Features

  • Access + Refresh Token Pattern: Short-lived access tokens with long-lived refresh tokens
  • Secure by Default: Tokens are hashed before storage (SHA-256)
  • Device Tracking: Optional device identifier per session
  • Rails Integration: Generator for migrations, configurable TTLs, controller helpers

Requirements

  • Ruby 3.3+
  • Rails 8.0+

Installation

Add to your Gemfile:

gem "seshlock"

Then run:

bundle install
rails generate seshlock:install
rails db:migrate

Configuration

The generator creates config/initializers/seshlock.rb:

Seshlock.configure do |config|
  config.access_token_ttl  = 15.minutes
  config.refresh_token_ttl = 30.days
end

Setup

1. Include UserMethods in your User model

class User < ApplicationRecord
  include Seshlock::UserMethods
  has_secure_password
end

2. Create a SessionsController

class SessionsController < ApplicationController
  include Seshlock::ControllerMethods

  # Skip any existing auth for login
  skip_before_action :authenticate!, only: [:login]

  # Protect these endpoints with the appropriate token type
  before_action :authenticate_with_seshlock_access_token!, only: [:ping]
  before_action :authenticate_with_seshlock_refresh_token!, only: [:logout, :refresh]

  # Handle Seshlock errors gracefully
  rescue_from Seshlock::Error, with: :render_seshlock_error

  # POST /login
  def login
    result = seshlock_login(
      email: params[:email],
      password: params[:password],
      device: params[:device]
    )
    render json: result, status: :ok
  end

  # POST /logout
  def logout
    seshlock_logout
    head :ok
  end

  # POST /refresh
  def refresh
    result = seshlock_refresh(device: params[:device])
    render json: result, status: :ok
  end

  # GET /ping (protected endpoint example)
  def ping
    render json: { message: "pong", user: current_seshlock_user.email }
  end
end

3. Add routes

Rails.application.routes.draw do
  post "login",   to: "sessions#login"
  post "logout",  to: "sessions#logout"
  post "refresh", to: "sessions#refresh"
  get  "ping",    to: "sessions#ping"
end

API Reference

Controller Methods

When you include Seshlock::ControllerMethods, you get:

Authentication (use as before_action)

  • authenticate_with_seshlock_access_token! - Validates Bearer token from Authorization header
  • authenticate_with_seshlock_refresh_token! - Validates refresh token from params[:refresh_token]

Instance Variables (set after authentication)

  • @current_seshlock_user - The authenticated User record
  • @current_seshlock_access_token - The AccessToken record
  • @current_seshlock_refresh_token - The RefreshToken record

Action Helpers

  • seshlock_login(email:, password:, device: nil) - Authenticates and issues tokens
  • seshlock_logout - Revokes the current refresh token
  • seshlock_refresh(device: nil) - Rotates tokens (revokes old, issues new)

Sessions Module

For programmatic access:

# Issue new tokens
token_pair = Seshlock::Sessions.issue_tokens_to(user: user, device: "iPhone")
token_pair.access_token              # => "abc123..."
token_pair.access_token_expires_at   # => Time
token_pair.refresh_token             # => "def456..."
token_pair.refresh_token_expires_at  # => Time

# Refresh (returns [access_token, expires_at] or nil)
result = Seshlock::Sessions.refresh(refresh_token: raw_token)

# Revoke
Seshlock::Sessions.revoke_refresh_token(raw_refresh_token: token)

User Methods

The Seshlock::UserMethods concern adds:

user.seshlock_refresh_tokens  # => has_many association
user.seshlock_access_tokens   # => has_many through refresh_tokens
user.issue_seshlock_session(device: nil)  # => TokenPair

# Session Management
user.seshlock_sessions                    # => Active sessions (most recent first)
user.revoke_all_seshlock_sessions!        # => Logout everywhere
user.revoke_other_seshlock_sessions!(except_refresh_token: token)  # => Logout other devices

Controller Session Management

List, revoke specific, or revoke multiple sessions:

class SessionsController < ApplicationController
  include Seshlock::ControllerMethods

  before_action :authenticate_with_seshlock_access_token!, 
                only: [:index, :destroy, :destroy_others]
  before_action :authenticate_with_seshlock_refresh_token!, 
                only: [:destroy_all]

  # GET /sessions - List all active sessions
  def index
    render json: { sessions: seshlock_list_sessions }
    # Returns: [{ id: 1, device: "iPhone", created_at: ..., expires_at: ..., current: true }, ...]
  end

  # DELETE /sessions/:id - Revoke specific session
  def destroy
    seshlock_revoke_session(session_id: params[:id])
    head :ok
  end

  # DELETE /sessions/others - Revoke all except current
  def destroy_others
    seshlock_revoke_others
    head :ok
  end

  # DELETE /sessions/all - Revoke all (requires refresh token)
  def destroy_all
    seshlock_revoke_all
    head :ok
  end
end

Error Handling

All errors inherit from Seshlock::Error:

Error When Raised Suggested HTTP Status
MissingTokenError No token provided 401 Unauthorized
InvalidTokenError Access token expired/revoked 401 Unauthorized
MalformedTokenError Authorization header malformed 401 Unauthorized
InvalidGrantError Refresh token expired/revoked 400 Bad Request
MissingCredentialsError Email/password not provided 400 Bad Request
InvalidCredentialsError Wrong email/password 401 Unauthorized

Use render_seshlock_error helper or implement your own:

rescue_from Seshlock::Error, with: :render_seshlock_error

Client Usage

Login

curl -X POST http://localhost:3000/login \
  -H "Content-Type: application/json" \
  -d '{"email": "[email protected]", "password": "secret"}'

Response:

{
  "user": { "email": "[email protected]" },
  "access_token": "abc123...",
  "access_token_expires_at": "2024-01-01T12:15:00Z",
  "refresh_token": "def456...",
  "refresh_token_expires_at": "2024-01-31T12:00:00Z"
}

Authenticated Request

curl http://localhost:3000/ping \
  -H "Authorization: Bearer abc123..."

Refresh Tokens

curl -X POST http://localhost:3000/refresh \
  -H "Content-Type: application/json" \
  -d '{"refresh_token": "def456..."}'

License

MIT License. See LICENSE for details.

About

Session token management for Rails applications.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Languages