โœจ Securing Your Rails 8 API ๐ŸŒ with Token ๐Ÿท -Based Vs JWT Authentication ๐Ÿ”‘

Modern web and mobile applications demand secure APIs. Traditional session-based authentication falls short in stateless architectures like RESTful APIs. This is where Token-Based Authentication and JWT (JSON Web Token) shine. In this blog post, we’ll explore both approaches, understand how they work, and integrate them into a Rails 8 application.

๐Ÿ” 1. What is Token-Based Authentication?

Token-based authentication is a stateless security mechanism where the server issues a unique, time-bound token after validating a user’s credentials. The client stores this token (usually in local storage or memory) and sends it along with each API request via HTTP headers.

โœ… Key Concepts:

  • Stateless: No session is stored on the server.
  • Scalable: Ideal for distributed systems.
  • Tokens can be opaque (random strings).

๐Ÿƒบ Algorithms used:

  • Token generation commonly uses SecureRandom.

๐Ÿ”Ž What is SecureRandom?

SecureRandom is a Ruby module that generates cryptographically secure random numbers and strings. It uses operating system facilities (like /dev/urandom on Unix or CryptGenRandom on Windows) to generate high-entropy values that are safe for use in security-sensitive contexts like tokens, session identifiers, and passwords.

For example:

SecureRandom.hex(32) # generates a 64-character hex string (256 bits)

In Ruby, if you encounter the error:

(irb):5:in '<main>': uninitialized constant SecureRandom (NameError)
Did you mean?  SecurityError

It means the SecureRandom module hasnโ€™t been loaded. Although SecureRandom is part of the Ruby Standard Library, it’s not automatically loaded in every environment. You need to explicitly require it.

โœ… Solution

Add the following line before using SecureRandom:

require 'securerandom'

Then you can use:

SecureRandom.hex(16)  # => "a1b2c3d4e5f6..."

๐Ÿ“š Why This Happens

Ruby does not auto-load all standard libraries to save memory and load time. Modules like SecureRandom, CSV, OpenURI, etc., must be explicitly required if you’re working outside of Rails (like in plain Ruby scripts or IRB).

In a Rails environment, require 'securerandom' is typically handled automatically by the framework.

๐Ÿ› ๏ธ Tip for IRB

If you’re experimenting in IRB (interactive Ruby shell), just run:

require 'securerandom'
SecureRandom.uuid  # or any other method

This will eliminate the NameError.

๐Ÿ”’ Why 256 bits?

A 256-bit token offers a massive keyspace of 2^256 combinations, making brute-force attacks virtually impossible. The higher the bit-length, the better the resistance to collision and guessing attacks. Most secure tokens range between 128 and 256 bits. While larger tokens are more secure, they consume more memory and storage.

โš ๏ธ Drawbacks:

  • SecureRandom tokens are opaque and must be stored on the server (e.g., in a database) for validation.
  • Token revocation requires server-side tracking.

๐Ÿ‘ท๏ธ Implementing Token-Based Authentication in Rails 8

Step 1: Generate User Model

rails g model User email:string password_digest:string token:string
rails db:migrate

Step 2: Add Secure Token

# app/models/user.rb
has_secure_password
has_secure_token :token

Step 3: Authentication Controller

# app/controllers/api/v1/sessions_controller.rb
class Api::V1::SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      user.regenerate_token
      render json: { token: user.token }, status: :ok
    else
      render json: { error: 'Invalid credentials' }, status: :unauthorized
    end
  end
end

Step 4: Protect API Endpoints

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  before_action :authenticate_user!

  private

  def authenticate_user!
    token = request.headers['Authorization']&.split(' ')&.last
    @current_user = User.find_by(token: token)
    render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
  end
end


๐Ÿ” 2. What is JWT (JSON Web Token)?

JWT is an open standard for secure information exchange, defined in RFC 7519.

๐Ÿ”— What is RFC 7519?

RFC 7519 is a specification by the IETF (Internet Engineering Task Force) that defines the structure and rules of JSON Web Tokens. It lays out how to encode claims in a compact, URL-safe format and secure them using cryptographic algorithms. It standardizes the way information is passed between parties as a JSON object.

Check: https://datatracker.ietf.org/doc/html/rfc7519

๐Ÿ“ˆ Structure of JWT:

A JWT has three parts:

  1. Header: Specifies the algorithm used (e.g., HS256) and token type (JWT).
  2. Payload: Contains claims (e.g., user_id, exp).
  3. Signature: Validates the token integrity using a secret or key.

Example:

eyeJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJleHAiOjE2ODk5OTk5OTl9.Dr2k1ehxw7qBKi_Oe-JogBxy...

๐Ÿš€ Why 3 Parts?

  • The Header informs the verifier of the cryptographic operations applied.
  • The Payload is the actual data transferred.
  • The Signature protects the data and ensures that the token hasn’t been modified.

This makes JWT self-contained, tamper-resistant, and easily verifiable without a server-side lookup.

โš–๏ธ JWT Algorithms in Detail

๐Ÿ“ HS256 (HMAC SHA-256)

HS256 stands for HMAC with SHA-256. It is a symmetric algorithm, meaning the same secret is used for signing and verifying the JWT.

  • HMAC: Hash-based Message Authentication Code combines a secret key with the hash function.
  • SHA-256: A 256-bit secure hash function that produces a fixed-length output.
โšก Why JWT uses HS256?
  • It’s fast and computationally lightweight.
  • Ensures that only someone with the secret can produce a valid signature.
  • Ideal for internal applications where the secret remains safe.

If your use case involves public key encryption, you should use RS256 (RSA) which uses asymmetric key pairs.

๐ŸŒŸ Advantages of JWT over Basic Tokens

FeatureToken-BasedJWT
Self-containedNoYes
Verifiable without DBNoYes
Expiry built-inNoYes
Tamper-proofLowHigh
ScalableMediumHigh

๐Ÿงฌ Deep Dive: The Third Part of a JWT โ€” The Signature

๐Ÿ“Œ What is the Third Part?

The third part of a JWT is the signature. It ensures data integrity and authenticity.

Structure of a JWT:

<base64url-encoded header>.<base64url-encoded payload>.<base64url-encoded signature>

Each section is Base64URL-encoded and joined with .

๐Ÿ” How is the Signature Generated?

The signature is created using a cryptographic algorithm like HS256, and it’s built like this:

HMACSHA256(
  base64UrlEncode(header) + "." + base64UrlEncode(payload),
  secret_key
)

โœ… Example:

Assume the following:

Header: {
  "alg": "HS256",
  "typ": "JWT"
}
Payload: {
  "user_id": 1,
  "exp": 1717777777
}
Secret key: "my$ecretKey"

  1. Base64URL encode header and payload:
    header = Base64.urlsafe_encode64('{"alg":"HS256","typ":"JWT"}') # eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

    payload = Base64.urlsafe_encode64('{"user_id":1,"exp":1717777777}') # eyJ1c2VyX2lkIjoxLCJleHAiOjE3MTc3Nzc3Nzd9
  2. Concatenate them with a period: data = "#{header}.#{payload}"
  3. Sign it with HMAC SHA-256 using your secret:
    signature = OpenSSL::HMAC.digest('sha256', 'my$ecretKey', data)
  4. Base64URL encode the result: Base64.urlsafe_encode64(signature)

Now your JWT becomes:

<encoded-header>.<encoded-payload>.<encoded-signature>

๐ŸŽฏ Why Is the Signature Crucial?
  • Tamper Detection: If someone changes the payload (e.g., user_id: 1 to user_id: 9999), the signature will no longer match, and verification will fail.
  • Authentication: Only the party with the secret key can generate a valid signature. This confirms the sender is trusted.
  • Integrity: Ensures the content of the token hasn’t been altered between issuance and consumption.
๐Ÿ” What If Signature Is Invalid?

When the server receives the token:

JWT.decode(token, secret, true, { algorithm: 'HS256' })

If the signature doesn’t match the header + payload:

  • It raises an error (JWT::VerificationError)
  • The request is rejected with 401 Unauthorized

โš™๏ธ Why Use HS256?

  • HS256 (HMAC with SHA-256) is fast and secure for symmetric use cases.
  • Requires only a single shared secret for encoding and decoding.
  • Ideal for internal systems or when you fully control both the issuer and verifier.

Great questions! Let’s break them down in simple, technical terms:


1๏ธโƒฃ What is a Digital Signature in JWT?

A digital signature is a way to prove that a piece of data has not been tampered with and that it came from a trusted source.

๐Ÿ” In JWT:

  • The signature is created using:
    • The Header (e.g. {"alg": "HS256", "typ": "JWT"})
    • The Payload (e.g. {"user_id": 1, "exp": 1717777777})
    • A secret key (known only to the issuer)

โœ… Is it encryption?

โŒ No, the signature does not encrypt the data.
โœ… It performs a one-way hash-based verification using algorithms like HMAC SHA-256.

HMACSHA256(base64url(header) + "." + base64url(payload), secret)

The result is a hash (signature), not encrypted data.

๐Ÿ“Œ What does “digitally signed” mean?

When a JWT is digitally signed, it means:

  • The payload was not altered after being issued
  • The token was created by someone who knows the secret key

2๏ธโƒฃ Can JWT Transfer Big JSON Payloads?

Technically, yes, but with trade-offs.

๐Ÿงพ Payload in JWT

The payload can be any JSON object:

{
  "user_id": 1,
  "role": "admin",
  "permissions": ["read", "write", "delete"],
  "data": { "long_array": [...], "details": {...} }
}

๐Ÿšง But Watch Out:

ConcernDescription
๐Ÿ”„ Token SizeJWTs are often stored in headers or cookies. Big payloads increase HTTP request size.
๐Ÿ” Not EncryptedAnyone who gets the token can read the payload unless it’s encrypted.
๐Ÿ’พ StorageBrowsers and mobile clients have limits (e.g., cookie size = ~4KB).
๐Ÿข PerformanceBigger payloads = slower parsing, transfer, and validation.

3๏ธโƒฃ Can We Encrypt a JWT?

Yes, but that requires JWE โ€” JSON Web Encryption (not just JWT).

โœจ JWT โ‰  Encrypted

A normal JWT is:

  • Signed (to prove authenticity & integrity)
  • Not encrypted (anyone can decode and read payload)
๐Ÿ” If You Want Encryption:

Use JWE (RFC 7516), which:

  • Encrypts the payload
  • Uses algorithms like AES, RSA-OAEP, etc.

However, JWE is less commonly used, as it adds complexity and processing cost.

โœ… Summary
FeatureJWT (Signed)JWE (Encrypted)
Data readable?YesNo
Tamper-proof?YesYes
Confidential?NoYes
Commonly used?โœ… Yesโš ๏ธ Less common
AlgorithmHMAC/RS256 (e.g. HS256)AES/RSA

๐Ÿ” What does “one-way hash-based verification using HMAC SHA-256” mean?

Let’s decode this phrase:

๐Ÿ’ก HMAC SHA-256 is:

  • HMAC = Hash-based Message Authentication Code
  • SHA-256 = Secure Hash Algorithm, 256-bit output

When combined:

HMAC SHA-256 = A cryptographic function that creates a hash (fingerprint) of some data using a secret key.

It does NOT encrypt the data. It does NOT encode the data. It just creates a fixed-length signature to prove authenticity and integrity.

๐Ÿ”’ One-Way Hashing (vs Encryption)

ConceptIs it reversible?PurposeExample Algo
๐Ÿ”‘ Encryptionโœ… Yes (with key)Hide dataAES, RSA
๐Ÿงช HashingโŒ NoProve data hasnโ€™t changedSHA-256
โœ๏ธ HMACโŒ NoSign data w/ secretHMAC SHA256

๐Ÿ” How HMAC SHA-256 is used in JWT (Detailed Example)

Let’s take:

header  = {"alg":"HS256","typ":"JWT"}
payload = {"user_id":123,"exp":1717700000}
secret  = "my$ecretKey"

๐Ÿ”น Step 1: Base64Url encode header and payload

base64_header  = Base64.urlsafe_encode64(header.to_json)
# => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"

base64_payload = Base64.urlsafe_encode64(payload.to_json)
# => "eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTcxNzcwMDAwMH0"

๐Ÿ”น Step 2: Concatenate them with a dot

data = "#{base64_header}.#{base64_payload}"
# => "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsImV4cCI6MTcxNzcwMDAwMH0"

๐Ÿ”น Step 3: Generate Signature using HMAC SHA-256

require 'openssl'
require 'base64'

signature = OpenSSL::HMAC.digest('sha256', secret, data)
# => binary format

encoded_signature = Base64.urlsafe_encode64(signature).gsub('=', '')
# => This is the third part of JWT
# => e.g., "NLoeHhY5jzUgKJGKJq-rK6DTHCKnB7JkPbY3WptZmO8"

โœ… Final JWT:

<header>.<payload>.<signature>

Anyone receiving this token can:

  • Recompute the signature using the same secret key
  • If it matches the one in the token, it’s valid
  • If it doesn’t match, the token has been tampered

โ“ Is SHA-256 used for encoding or encrypting?

โŒ SHA-256 is not encryption.
โŒ SHA-256 is not encoding either.
โœ… It is a hash function: one-way and irreversible.

It’s used in HMAC to sign data (prove data integrity), not to encrypt or hide data.

โœ… Summary:

PurposeSHA-256 / HMAC SHA-256
Encrypts data?โŒ No
Hides data?โŒ No (use JWE for that)
Reversible?โŒ No
Used in JWT?โœ… Yes (for signature)
Safe?โœ… Very secure if secret is strong

๐ŸŽฏ First: The Big Misunderstanding โ€” Why JWT Isn’t “Encrypted”

JWT is not encrypted by default.

It is just encoded + signed.
You can decode the payload, but you cannot forge the signature.

๐Ÿง  Difference Between Encoding, Encryption, and Hashing

ConceptPurposeReversible?Example
EncodingMake data safe for transmissionโœ… YesBase64
EncryptionHide data from unauthorized eyesโœ… Yes (with key)AES, RSA
HashingVerify data hasn’t changedโŒ NoSHA-256, bcrypt

๐Ÿ”“ Why can JWT payload be decoded?

Because the payload is only Base64Url encoded, not encrypted.

Example:

{
  "user_id": 123,
  "role": "admin"
}

When sent in JWT, it becomes:

eyJ1c2VyX2lkIjoxMjMsInJvbGUiOiJhZG1pbiJ9

โœ… You can decode it with any online decoder. Itโ€™s not private, only structured and verifiable.

๐Ÿ” Then What Protects the JWT?

The signature is what protects it.

  • It proves the payload hasnโ€™t been modified.
  • The backend signs it with a secret key (HMAC SHA-256 or RS256).
  • If anyone tampers with the payload and doesn’t have the key, they canโ€™t generate a valid signature.

๐Ÿงพ Why include the payload inside the JWT?

This is the brilliant part of JWT:

  • The token is self-contained.
  • You donโ€™t need a database lookup on every request.
  • You can extract data like user_id, role, permissions right from the token!

โœ… So yes โ€” it’s just a token, but a smart token with claims (data) you can trust.

This is ideal for stateless APIs.

๐Ÿ’ก Then why not send payload in POST body?

You absolutely can โ€” and often do, for data-changing operations (like submitting forms). But thatโ€™s request data, not authentication info.

JWT serves as the proof of identity and permission, like an ID card.

You put it in the Authorization header, not the body.

๐Ÿ“ฆ Is it okay to send large payloads in JWT?

Technically, yes, but not recommended. Why?

  • JWTs are sent in every request header โ€” that adds bloat.
  • Bigger tokens = slower transmission + possible header size limits.
  • Keep payload minimal: only whatโ€™s necessary (user id, roles, permissions, exp).

If your payload is very large, use a token to reference it in DB or cache, not store everything in the token.

โš ๏ธ If the secret doesnโ€™t match?

Yes โ€” that means someone altered the token (probably the payload).

  • If user_id was changed to 999, but they canโ€™t recreate a valid signature (they donโ€™t have the secret), the backend rejects the token.

๐Ÿ” Then When Should We Encrypt?

JWT only signs, but not encrypts.

If you want to hide the payload:

  • Use JWE (JSON Web Encryption) โ€” a different standard.
  • Or: don’t put sensitive data in JWT at all.

๐Ÿ” Summary: Why JWT is a Big Deal

  • โœ… Self-contained authentication
  • โœ… Stateless (no DB lookups)
  • โœ… Signed โ€” so payload can’t be tampered
  • โŒ Not encrypted โ€” anyone can see payload
  • โš ๏ธ Keep payload small and non-sensitive

๐Ÿง  One Last Time: Summary Table

TopicJWTPOST Body
Used forAuthentication/identitySubmitting request data
Data typeClaims (user_id, role)Form/input data
Seen by user?Yes (Base64-encoded)Yes
SecuritySignature w/ secretHTTPS
Stored where?Usually in browser (e.g. localStorage, cookie)N/A

Think of JWT like a sealed letter:

  • Anyone can read the letter (payload).
  • But they can’t forge the signature/stamp.
  • The receiver checks the signature to verify the letter is real and unmodified.

๐Ÿงจ Yes, JWT Payload is Visible โ€” and That Has Implications

The payload of a JWT is only Base64Url encoded, not encrypted.

This means anyone who has the token (e.g., a user, a man-in-the-middle without HTTPS, or a frontend dev inspecting in the browser) can decode it and see:

{
  "user_id": 123,
  "role": "admin",
  "permissions": ["read", "write", "delete"],
  "email": "user@example.com"
}

๐Ÿ” Is This a Security Risk?

It depends on what you put inside the payload.

โœ… Safe things to include:

  • user_id
  • exp (expiration timestamp)
  • Minimal role or scope info like "role": "user"

โŒ Do not include sensitive data:

  • Email addresses (if private)
  • Password hashes (never!)
  • Credit card info
  • Internal tokens or database keys
  • Personally Identifiable Info (PII)

๐Ÿ”Ž So Why Do People Still Use JWT?

JWT is great when used correctly:

  • It doesn’t prevent others from reading the payload, but it prevents them from modifying it (thanks to the signature).
  • It allows stateless auth without needing a DB lookup on every request.
  • It’s useful for microservices where services can verify tokens without a central auth store.

๐Ÿงฐ Best Practices for JWT Payloads

  1. Treat the payload as public data.
    • Ask yourself: โ€œIs it okay if the user sees this?โ€
  2. Never trust the token blindly on the client.
    • Always verify the signature and claims server-side.
  3. Use only identifiers, not sensitive context.
    • For example, instead of embedding full permissions: { "user_id": 123, "role": "admin" } fetch detailed permissions on the backend based on role.
  4. Encrypt the token if sensitive data is needed.
    • Use JWE (JSON Web Encryption), or
    • Store sensitive data on the server and pass only a reference (like a session id or user_id).

๐Ÿ“Œ Bottom Line

JWT is not private. It is only protected from tampering, not from reading.

So if you use it in your app, make sure the payload contains only safe, public information, and that any sensitive logic (like permission checks) happens on the server.


๐Ÿš€ Integrating JWT in Rails 8

Step 1: Add Gem

gem 'jwt'

Step 2: Generate Secret Key

# config/initializers/jwt.rb
JWT_SECRET = Rails.application.credentials.jwt_secret || 'your_dev_secret_key'

Step 3: Authentication Logic

# app/services/json_web_token.rb
class JsonWebToken
  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, JWT_SECRET, 'HS256')
  end

  def self.decode(token)
    body = JWT.decode(token, JWT_SECRET, true, { algorithm: 'HS256' })[0]
    HashWithIndifferentAccess.new body
  rescue
    nil
  end
end

Step 4: Sessions Controller for JWT

# app/controllers/api/v1/sessions_controller.rb
class Api::V1::SessionsController < ApplicationController
  def create
    user = User.find_by(email: params[:email])
    if user&.authenticate(params[:password])
      token = JsonWebToken.encode(user_id: user.id)
      render json: { jwt: token }, status: :ok
    else
      render json: { error: 'Invalid credentials' }, status: :unauthorized
    end
  end
end

Step 5: Authentication in Application Controller

# app/controllers/application_controller.rb
class ApplicationController < ActionController::API
  before_action :authenticate_request

  def authenticate_request
    header = request.headers['Authorization']
    token = header.split(' ').last if header
    decoded = JsonWebToken.decode(token)
    @current_user = User.find_by(id: decoded[:user_id]) if decoded
    render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
  end
end

๐ŸŒ How Token-Based Authentication Secures APIs

๐Ÿ”’ Benefits:

  • Stateless: Scales well
  • Works across domains
  • Easy to integrate with mobile/web clients
  • JWT is tamper-proof and verifiable

โšก Drawbacks:

  • Token revocation is hard without server tracking (esp. JWT)
  • Long-lived tokens can be risky if leaked
  • Requires HTTPS always

๐Ÿ“† Final Thoughts

For most Rails API-only apps, JWT is the go-to solution due to its stateless, self-contained nature. However, for simpler setups or internal tools, basic token-based methods can still suffice. Choose based on your app’s scale, complexity, and security needs.


Happy coding! ๐Ÿš€

Guide: Railsย 8 API Application โ€“ Authentication ๐Ÿ” mechanisms | Sample Rails API app with Rspec Test cases

When building a Rails API app, you typically need token-based authentication instead of cookie-based sessions (which are more common in full-stack Rails apps). Here are the most common authentication mechanisms you can use in a Rails API-only application:

๐Ÿ” 1. Token-Based Authentication

Most Common & Recommended for APIs

a. JWT (JSON Web Tokens)

  • Gems: jwt, knock, devise-jwt
  • How it works: After login, the server issues a JWT token which the client must include in the Authorization header (Bearer <token>) in subsequent requests.
  • Pros:
    • Stateless, scalable.
    • Widely supported across mobile and frontend frameworks.
  • Cons:
    • Tokens can’t be invalidated easily without extra measures (e.g., a blacklist).

b. Token-based Auth with Devise + TokenAuthenticatable

  • Gems: devise_token_auth
  • Uses Devise under the hood.
  • Stores tokens on the server (in DB), enabling logout and token revocation.
  • Compatible with React Native and SPAs.

๐Ÿ” 2. OAuth 2.0 / OmniAuth (for Third-party Logins)

  • Gems: omniauth, doorkeeper
  • Use when you want users to log in via:
    • Google
    • Facebook
    • GitHub
  • Doorkeeper is often used to implement OAuth 2 provider (if youโ€™re exposing your API to other apps).
  • Best when integrating external identity providers.

๐Ÿ” 3. API Key Authentication

  • Useful for machine-to-machine communication or when exposing APIs to third-party developers.
  • Each user/client is assigned a unique API key.
  • Example: Authorization: Token token=abc123
  • You store the API key in the DB and verify it on each request.
  • Lightweight and easy to implement.

๐Ÿ” 4. HTTP Basic Authentication

  • Simple and built-in with Rails (authenticate_or_request_with_http_basic).
  • Not suitable for production unless combined with HTTPS and only used for internal/testing tools.

๐Ÿ‘‰๐Ÿป Choosing the Right Auth Mechanism

Use CaseRecommended Method
Mobile app or frontend SPAJWT (devise-jwt / knock)
Internal API between servicesAPI key
Want email/password with token authdevise_token_auth
External login via Google/GitHubomniauth + doorkeeper
OAuth2 provider for third-party devsdoorkeeper
Quick-and-dirty internal authHTTP Basic Auth

๐Ÿ”„ How JWT Authentication Works โ€” Step by Step

1. User Logs In

  • The client (e.g., React app, mobile app) sends a POST /login request with email/password.
  • Your Rails API validates the credentials.
  • If valid, it generates a JWT token and sends it back to the client.
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

2. Client Stores the Token

  • The client stores the token in localStorage, sessionStorage, or memory (for SPAs), or a secure storage for mobile apps.

3. Client Sends Token on Requests

  • For any subsequent request to protected resources, the client includes the JWT in the Authorization header:
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

4. Server Verifies the Token

  • Rails extracts the token, decodes it using a secret key, and verifies:
    • The signature is valid.
    • The token is not expired.
    • The user ID (or sub claim) is valid.

If everything checks out, the request is allowed to proceed.

5. Token Expiration

  • Tokens usually include an exp (expiration) claim, e.g., 15 minutes, 1 hour, etc.
  • After expiration, the client must log in again or use a refresh token flow if supported.

๐Ÿ”’ Security: Is JWT Secure?

JWT can be secure, if used correctly. Here’s a breakdown:

โœ… Security Benefits

FeatureWhy It Helps
StatelessNo session storage needed; scales easily
SignedThe token is signed (HMAC or RSA), so it canโ€™t be tampered with
CompactSent in headers; easy to pass around
Exp claimTokens expire automatically after a period

โš ๏ธ Security Considerations

IssueDescriptionMitigation
Token theftIf an attacker steals the token, they can impersonate the user.Always use HTTPS. Avoid storing tokens in localStorage if possible.
No server-side revocationTokens canโ€™t be invalidated until they expire.Use short-lived access tokens + refresh tokens or token blacklist (DB).
Long token lifespanLonger expiry means higher risk if leaked.Keep exp short (e.g., 15โ€“30 min). Use refresh tokens if needed.
Poor secret handlingIf your secret key leaks, anyone can forge tokens.Store your JWT_SECRET in environment variables, never in code.
JWT stored in localStorageSusceptible to XSS attacks in web apps.Use HttpOnly cookies when possible, or protect against XSS.
Algorithm confusionAttacker could force a weak algorithm.Always validate the algorithm (alg) on decoding. Use only HMAC or RSA.

๐Ÿงช Example Token (Decoded)

A typical JWT has three parts:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJ1c2VyX2lkIjoxLCJleHAiOjE3MDAwMDAwMDB9.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

Breakdown:

  1. Header (Base64-encoded JSON)
{
  "alg": "HS256",
  "typ": "JWT"
}

  1. Payload
{
  "user_id": 1,
  "exp": 1700000000
}

  1. Signature
  • HMAC-SHA256 hash of header + payload + secret key.

๐Ÿ›ก Best Practices for JWT in Rails API

  • Use devise-jwt or knock to handle encoding/decoding securely.
  • Set short token lifetimes (exp claim).
  • Use HTTPS only.
  • Consider implementing refresh tokens for session continuation.
  • Avoid token storage in localStorage unless you trust your frontend.
  • Rotate secrets periodically (invalidate tokens when secrets change).

Now Let’s create a sample Rails API application and test what we learned.

๐Ÿงฑ Sample Rails API web app: Prerequisites

  • A Rails 8 app with --api mode enabled: rails new my_api_app --api
  • A User model with email and password_digest.
  • We’ll use bcrypt for password hashing.

โœ… Step 1: Add Required Gems

In your Gemfile:

gem 'jwt'
gem 'bcrypt'

Then run:

bundle install

โœ… Step 2: Generate the User Model

rails g model User email:string password_digest:string
rails db:migrate

In app/models/user.rb:

class User < ApplicationRecord
  has_secure_password
end

Now you can create users with secure passwords.

โœ… Step 3: Create JWT Helper Module

Create a service object or helper to encode/decode tokens.

app/lib/json_web_token.rb (create the lib folder if needed):

# app/lib/json_web_token.rb
class JsonWebToken
  SECRET_KEY = Rails.application.credentials.secret_key_base

  def self.encode(payload, exp = 24.hours.from_now)
    payload[:exp] = exp.to_i
    JWT.encode(payload, SECRET_KEY)
  end

  def self.decode(token)
    decoded = JWT.decode(token, SECRET_KEY)[0]
    HashWithIndifferentAccess.new(decoded)
  rescue JWT::DecodeError => e
    nil
  end
end

โœ… Step 4: Create the Authentication Controller

rails g controller auth

app/controllers/auth_controller.rb:

class AuthController < ApplicationController
  def login
    user = User.find_by(email: params[:email])

    if user&.authenticate(params[:password])
      token = JsonWebToken.encode(user_id: user.id)
      render json: { token: token }, status: :ok
    else
      render json: { error: 'Invalid credentials' }, status: :unauthorized
    end
  end
end

โœ… Step 5: Protect Other Endpoints with Authentication

Make a reusable authenticate_request method.

app/controllers/application_controller.rb:

class ApplicationController < ActionController::API
  before_action :authenticate_request

  attr_reader :current_user

  private

  def authenticate_request
    header = request.headers['Authorization']
    token = header.split(' ').last if header.present?

    if token
      decoded = JsonWebToken.decode(token)
      @current_user = User.find_by(id: decoded[:user_id]) if decoded
    end

    render json: { error: 'Unauthorized' }, status: :unauthorized unless @current_user
  end
end

Now all your controllers inherit this behaviour unless you skip_before_action.

โœ… Step 6: Add Routes

config/routes.rb:

Rails.application.routes.draw do
  post '/login', to: 'auth#login'

  get '/profile', to: 'users#profile' # Example protected route
end

โœ… Step 7: Example Protected Controller

rails g controller users

app/controllers/users_controller.rb:

class UsersController < ApplicationController
  def profile
    render json: { id: current_user.id, email: current_user.email }
  end
end

๐Ÿงช Test It Out (Example)

Step 1: Create a User (via Rails Console)

User.create!(email: "test@example.com", password: "password123")

Step 2: Login via POST /login

POST /login
Content-Type: application/json

{
  "email": "test@example.com",
  "password": "password123"
}

Response:

{ "token": "eyJhbGciOi..." }

Step 3: Use Token in Authenticated Request

GET /profile
Authorization: Bearer eyJhbGciOi...

๐Ÿ”’ Extras You Might Add Later

  • Token expiration errors
  • Refresh tokens
  • Token revocation (e.g., a blacklist table)
  • Roles/permissions inside the token (e.g., admin claims)

Let’s now write RSpec tests for the JWT-based authentication flow we just set up in your Rails API app.

Assumptions

  • You already have:
    • A User model with email and password_digest
    • An AuthController with login
    • A UsersController with a protected profile action
    • JWT auth logic in JsonWebToken

๐Ÿ”ง Step 1: Add RSpec & Factory Bot

In your Gemfile (if not already added):

group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
end

group :test do
  gem 'faker'
end

Then install:

bundle install
rails generate rspec:install


๐Ÿญ Step 2: Setup Factory for User

spec/factories/users.rb:

FactoryBot.define do
  factory :user do
    email { Faker::Internet.email }
    password { 'password123' }
    password_confirmation { 'password123' }
  end
end


๐Ÿงช Step 3: Auth Request Specs

spec/requests/auth_spec.rb:

require 'rails_helper'

RSpec.describe 'Authentication', type: :request do
  let!(:user) { create(:user, password: 'password123') }

  describe 'POST /login' do
    context 'with valid credentials' do
      it 'returns a JWT token' do
        post '/login', params: { email: user.email, password: 'password123' }

        expect(response).to have_http_status(:ok)
        expect(JSON.parse(response.body)).to include('token')
      end
    end

    context 'with invalid credentials' do
      it 'returns unauthorized' do
        post '/login', params: { email: user.email, password: 'wrong' }

        expect(response).to have_http_status(:unauthorized)
        expect(JSON.parse(response.body)).to include('error')
      end
    end
  end
end


๐Ÿ”’ Step 4: Profile (Protected) Request Specs

spec/requests/users_spec.rb:

require 'rails_helper'

RSpec.describe 'Users', type: :request do
  let!(:user) { create(:user) }
  let(:token) { JsonWebToken.encode(user_id: user.id) }

  describe 'GET /profile' do
    context 'with valid token' do
      it 'returns user profile' do
        get '/profile', headers: { 'Authorization' => "Bearer #{token}" }

        expect(response).to have_http_status(:ok)
        json = JSON.parse(response.body)
        expect(json['email']).to eq(user.email)
      end
    end

    context 'without token' do
      it 'returns unauthorized' do
        get '/profile'
        expect(response).to have_http_status(:unauthorized)
      end
    end

    context 'with invalid token' do
      it 'returns unauthorized' do
        get '/profile', headers: { 'Authorization' => 'Bearer invalid.token' }
        expect(response).to have_http_status(:unauthorized)
      end
    end
  end
end

๐Ÿ“ฆ Final Tips

  • Run tests with: bundle exec rspec
  • You can stub JsonWebToken.decode in unit tests if needed to isolate auth logic.


Exploring Rails 8: Powerful ๐Ÿ’ช Features, Deployment & Real-Time Updates

Introduction

Rails 8.x has arrived, bringing exciting new features and enhancements to improve productivity, performance, and ease of development. From built-in authentication to real-time WebSocket updates, this latest version of Rails continues its commitment to being a powerful and developer-friendly framework.

Let’s dive into some of the most significant features and improvements introduced in Rails 8.


Rails 8 Features & Enhancements

1. Modern JavaScript with Importmaps & Hotwire

Rails 8 eliminates the need for Webpack and Node.js, allowing developers to manage JavaScript dependencies more efficiently. Importmaps simplify dependency management by fetching JavaScript packages directly and caching them locally, removing runtime dependencies.

Key Benefits:

  • Faster page loads and reduced complexity
  • No need for Node.js or Webpack
  • Dependencies are cached locally and loaded efficiently

Example: Pinning a Package

bin/importmap pin local-time

This command fetches the package from npm and stores it locally for future use.

Hotwire Integration

Hotwire enables dynamic page updates without requiring heavy JavaScript frameworks. Rails 8 fully integrates Turbo and Stimulus, making frontend interactivity more seamless.

Importing Dependencies in application.js:
import "trix";

With this setup, developers can create reactive UI elements with minimal JavaScript.


2. Real-Time WebSockets with Action Cable & Turbo Streams

Rails 8 enhances real-time functionality with Action Cable and Turbo Streams, allowing WebSocket-based updates across multiple pages without additional JavaScript libraries.

Setting Up Turbo Streams in Views:

<%= turbo_stream_from @object %>

This creates a WebSocket channel tied to the object.

Broadcasting Updates from Models:

broadcast_to :object, render(partial: "objects/object", locals: { object: self })

Any changes to the object will be instantly reflected across all connected clients.

Why This Matters:

  • No need for third-party WebSocket npm packages
  • Real-time updates are built into Rails
  • Simplifies building interactive applications

3. Rich Text with ActionText

Rails 8 continues to support ActionText, making it easy to handle rich text content within models and views.

Model Level Implementation:

has_rich_text :body

This enables rich text storage and formatting for the body attribute of a model.

View Implementation:

<%= form.rich_text_area :body %>

This adds a full-featured WYSIWYG text editor to the form, allowing users to create and edit rich text content seamlessly.

Displaying Updated Timestamps:

<%= time_tag post.updated_at %>

This helper formats timestamps cleanly, improving date and time representation in views.


4. Deployment with Kamal โ€“ Simpler & Faster

Rails 8 introduces Kamal, a modern deployment tool that simplifies remote deployment by leveraging Docker containers.

Deployment Steps:

  1. Setup Remote Serverkamal setup
    • Installs Docker (if missing) and configures the server.
  2. Deploy the Applicationkamal deploy
    • Builds and ships a Docker container using Railsโ€™ default Dockerfile.

File Uploads with Active Storage

By default, Kamal stores uploaded files in Docker volumes, but this can be customized based on specific deployment needs.


5. Built-in Authentication โ€“ No Devise Needed

Rails 8 introduces native authentication, reducing reliance on third-party gems like Devise. This built-in system manages password encryption, user sessions, and password resets while keeping signup flows flexible.

Generating Authentication:

rails g authentication
rails db:migrate

Creating a User for Testing:

User.create(email: "user@example.com", password: "securepass")

Managing Authentication:

  • Uses bcrypt for password encryption
  • Provides a pre-built sessions_controller for handling authentication
  • Allows remote database changes via: kamal console

6. Turning a Rails App into a PWA

Rails 8 makes it incredibly simple to transform any app into a Progressive Web App (PWA), enabling offline support and installability.

Steps to Enable PWA:

  1. Modify application.html.erb: <%= tag.link pwa_manifest_path %>
  2. Ensure manifest and service-worker routes are enabled.
  3. Verify PWA files: pwa/manifest.json.erb and pwa/service-worker.js.
  4. Deploy and restart the application to see the Install button in the browser.

Final Thoughts

Rails 8 is packed with developer-friendly features that improve security, real-time updates, and deployment workflows. With Hotwire, Kamal, and native authentication, itโ€™s clear that Rails is evolving to reduce dependencies while enhancing performance.

Are you excited about Rails 8? Let me know your thoughts and experiences in the comments below!