惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

博客园 - 【当耐特】
K
Kaspersky official blog
V
Vulnerabilities – Threatpost
Hacker News - Newest:
Hacker News - Newest: "LLM"
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Secure Thoughts
I
Intezer
TaoSecurity Blog
TaoSecurity Blog
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
Spread Privacy
Spread Privacy
A
About on SuperTechFans
NISL@THU
NISL@THU
The GitHub Blog
The GitHub Blog
Hugging Face - Blog
Hugging Face - Blog
S
Security @ Cisco Blogs
S
SegmentFault 最新的问题
G
Google Developers Blog
B
Blog
N
News and Events Feed by Topic
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Google DeepMind News
Google DeepMind News
V2EX - 技术
V2EX - 技术
V
Visual Studio Blog
MyScale Blog
MyScale Blog
Webroot Blog
Webroot Blog
Vercel News
Vercel News
IT之家
IT之家
Microsoft Security Blog
Microsoft Security Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
S
Security Affairs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Stack Overflow Blog
Stack Overflow Blog
P
Proofpoint News Feed
L
Lohrmann on Cybersecurity
博客园 - 叶小钗
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Know Your Adversary
Know Your Adversary
T
Tailwind CSS Blog
F
Fortinet All Blogs
D
DataBreaches.Net
博客园 - Franky
博客园_首页
H
Heimdal Security Blog
宝玉的分享
宝玉的分享
阮一峰的网络日志
阮一峰的网络日志
Attack and Defense Labs
Attack and Defense Labs
Project Zero
Project Zero
雷峰网
雷峰网

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Build a Secure API with Rails 8 - Part-2: Authentication Foundations
Renzo Diaz · 2026-05-14 · via DEV Community

Hey folks 👋

Welcome back. In Part 1 we walked through the 11 attack vectors that shape every decision in this series. If you skipped it, please go read it first, because everything we do from now on is a direct response to one of those threats. Without that context, the code below is just another tutorial.

In this part we are going to start writing the API. By the end you will have a Rails 8 project with user registration, login, and token-based authentication using OAuth2 + JWT, with tokens stored safely in HttpOnly cookies instead of localStorage.

I want to be honest about something. When I first built this, I tried to do "everything at once". I added authentication, authorization, rate limiting, and serializers in the same commit, and I got lost. So in this series we are going slow on purpose. Part 2 is only about laying the foundation correctly. We will not finish every mitigation today, and that's fine.

To help us stay oriented, I'll keep a small progress tracker at the end of each post.

What we are building in Part 2

A small Rails 8 API with:

  • A User model with hashed passwords (no plain text, ever)
  • OAuth2 password grant flow so the client can exchange email + password for a token
  • JWT access tokens that the server can verify without hitting the database
  • Refresh tokens with short-lived access tokens
  • Tokens delivered through encrypted HttpOnly cookies, not JSON bodies the frontend has to store manually

If you've never touched Devise or Doorkeeper before, don't worry. I'll explain why we use each piece, not just how.

Prerequisites

  • Ruby on Rails 8
  • PostgreSQL 14+
  • Postman, Insomnia, or curl for testing

Step 1. Create the project in API mode

Rails has a built-in flag to skip all the browser-only middleware (cookies are gone by default, ERB views are gone, asset pipeline is gone). That's what we want for an API.

rails new secure_api_auth -T -d postgresql --api

Enter fullscreen mode Exit fullscreen mode

Breaking that down:

  • -T skips the default test suite (we'll set up testing later in the series)
  • -d postgresql uses Postgres instead of SQLite
  • --api strips out browser-oriented middleware

Then:

cd secure_api_auth

Enter fullscreen mode Exit fullscreen mode

Tip from experience: commit right here as "Initial commit" before you change anything. When something breaks two hours from now, having a clean baseline to git diff against will save you.

Step 2. Add the gems we need

Open your Gemfile and add:

# Database
gem 'pg'

# Password hashing
gem 'bcrypt', '~> 3.1.7'

# Authentication & Authorization
gem 'devise'
gem 'doorkeeper'
gem 'doorkeeper-jwt'

Enter fullscreen mode Exit fullscreen mode

Then:

bundle install

Enter fullscreen mode Exit fullscreen mode

A quick mental model before we go further, because these three gems confused me for a long time:

  • Devise owns the user identity. It knows how to store a user, hash a password, and verify "is this the right password for this email?"
  • Doorkeeper owns the access decision. After Devise confirms who you are, Doorkeeper issues the token that proves you are allowed to call the API.
  • doorkeeper-jwt changes the format of that token from a random string to a JWT, so the server can verify it without a database lookup.

In other words: Devise checks the ID at the door, Doorkeeper hands you the wristband, and JWT is what the wristband is made of.

Step 3. Install Devise

bin/rails generate devise:install

Enter fullscreen mode Exit fullscreen mode

Devise will print a few setup instructions. Since we're API-only, we only care about one of them: setting the default URL options for development.

Open config/environments/development.rb and add:

config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

Enter fullscreen mode Exit fullscreen mode

We won't be sending real emails in this tutorial, but Devise complains if this isn't set.

Generate the User model

bin/rails generate devise User
bin/rails db:migrate

Enter fullscreen mode Exit fullscreen mode

This creates a users table with an encrypted_password column (and a few others for tracking sign-in counts and lockouts).

🛡️ Mitigation in action: Token Theft and Password Breaches (Part 1, vectors 4 and 10)

Devise hashes passwords with bcrypt, which is intentionally slow. Even if an attacker dumps your database, they can't reverse the hashes. Each password also gets a unique salt, so two users who pick the same password end up with completely different hashes. This is why we never, ever store passwords in plain text.

Switch Devise to API mode

By default Devise wants to redirect users to HTML pages after login. We need to turn that off.

Open config/initializers/devise.rb and add (or modify) these two lines:

Devise.setup do |config|
  # ... keep everything else as generated ...

  # Don't use session storage for API authentication
  config.skip_session_storage = [:http_auth, :params_auth]

  # No HTML redirects, we return JSON
  config.navigational_formats = []
end

Enter fullscreen mode Exit fullscreen mode

Step 4. Install Doorkeeper

bin/rails generate doorkeeper:install
bin/rails generate doorkeeper:migration

Enter fullscreen mode Exit fullscreen mode

Before we run that migration, we need to edit it. Doorkeeper is built for the full OAuth2 flow (the one where you click "Log in with GitHub" and get redirected). We don't need that. We need the simpler password grant flow, where the client sends email + password directly and gets a token back. That requires loosening two null: false constraints.

Open the generated migration file (something like db/migrate/XXXXX_create_doorkeeper_tables.rb):

# In the oauth_applications table: allow null redirect_uri
t.text :redirect_uri  # remove the `null: false`

# In the oauth_access_tokens table: allow null application reference
t.references :application  # remove the `null: false`

# At the bottom of the file, link tokens back to users
add_foreign_key :oauth_access_grants, :users, column: :resource_owner_id
add_foreign_key :oauth_access_tokens, :users, column: :resource_owner_id

Enter fullscreen mode Exit fullscreen mode

Then migrate:

bin/rails db:migrate

Enter fullscreen mode Exit fullscreen mode

A word on the password grant flow. The OAuth2 spec actually discourages this flow for third-party clients, because it requires the client to handle the user's raw password. But for first-party clients (your own mobile app, your own SPA), it's perfectly reasonable, and it's what most "log in with email and password" APIs do under the hood. The key word is first-party: you control both ends.

Step 5. Re-enable cookies in API mode

This is the part that surprised me the first time. When you pass --api to rails new, Rails removes the cookie middleware. That makes sense, an API doesn't usually need cookies. But we do want them, because we want to store the access token in an HttpOnly cookie instead of letting JavaScript handle it.

Open config/application.rb:

module SecureApiAuth
  class Application < Rails::Application
    config.load_defaults 8.0
    config.api_only = true

    # Re-add cookie middleware so we can use HttpOnly cookie auth
    config.middleware.use ActionDispatch::Cookies
    config.middleware.use ActionDispatch::Session::CookieStore,
                          key: '_secure_api_session',
                          same_site: :lax,
                          secure: Rails.env.production?
  end
end

Enter fullscreen mode Exit fullscreen mode

🛡️ Mitigation in action: XSS and Token Theft (Part 1, vectors 1 and 10)

This is the single most important decision in this whole post. If you store a JWT in localStorage, any piece of JavaScript that runs on your page can read it. One XSS bug, one compromised npm dependency, one browser extension, and the token is gone.

HttpOnly cookies are invisible to JavaScript. The browser sends them automatically with every request, but document.cookie will not show them. Secure ensures the cookie is only sent over HTTPS. SameSite=Lax is our first line of defense against CSRF (which we'll fully address in a later part).

Step 6. Teach Doorkeeper to read tokens from the cookie

Out of the box, Doorkeeper looks for tokens in the Authorization: Bearer ... header. We need to add a new place for it to look: our encrypted cookie.

Create lib/doorkeeper/from_cookie.rb:

module Doorkeeper
  module FromCookie
    module_function

    def call(request)
      # Read the encrypted access token from the HttpOnly cookie
      request.cookie_jar.encrypted[:access_token]
    end
  end
end

Enter fullscreen mode Exit fullscreen mode

Why encrypted and not just signed?

Rails offers two protected cookie jars: signed (tamper-proof but readable) and encrypted (tamper-proof AND unreadable). If someone opens DevTools and copies the raw cookie value, with signed they'd see the JWT in plain text. With encrypted they see AES-256 garbage. The JWT is already protected by its own signature, but defense in depth is cheap here, so we use encrypted.

Step 7. Configure Doorkeeper

Open config/initializers/doorkeeper.rb and replace the generated content with:

# frozen_string_literal: true

require_relative '../../lib/doorkeeper/from_cookie'

Doorkeeper.configure do
  orm :active_record
  api_only

  # Resource Owner Password Credentials grant.
  # Lets users log in with email + password directly.
  grant_flows %w[password]
  allow_blank_redirect_uri true

  # How to find the user when they log in.
  resource_owner_from_credentials do |_routes|
    user = User.find_for_database_authentication(email: params[:email])
    user if user&.valid_password?(params[:password])
  end

  skip_client_authentication_for_password_grant true

  # Short-lived access tokens.
  access_token_expires_in 15.minutes

  # Enable refresh tokens.
  use_refresh_token

  # Use JWT format for access tokens.
  access_token_generator 'Doorkeeper::JWT'

  # Scopes. We'll use these later for authorization.
  default_scopes :read
  optional_scopes :write, :admin

  base_controller 'ApplicationController'

  # Token lookup order: 1) our cookie, 2) Bearer header, 3) query param.
  access_token_methods Doorkeeper::FromCookie,
                       :from_bearer_authorization,
                       :from_access_token_param
end

Enter fullscreen mode Exit fullscreen mode

A few of these settings deserve a closer look:

access_token_expires_in 15.minutes is intentional. If a token leaks, the attacker only has 15 minutes before it stops working. The refresh token (which lives longer) covers the user experience side, so they don't have to log in every 15 minutes.

use_refresh_token enables the rotation pattern: when an access token expires, the client uses a refresh token to get a new one without prompting the user. We'll wire up rotation more carefully in a later part.

access_token_methods order matters here. Doorkeeper checks the cookie first, then the Authorization header, then a query parameter. In production I'd actually remove from_access_token_param because tokens in URLs end up in server logs and browser history (Part 1, vector 10). For now I'm leaving it in so you can test easily with curl, but treat it as a TODO.

🛡️ Mitigation in action: Token Theft (Part 1, vector 10)

Short access token lifetime plus refresh token rotation is the standard pattern for limiting blast radius when a token leaks. We'll harden this further by adding revocation when we build the /logout endpoint.

Step 8. Configure the JWT payload

Create config/initializers/doorkeeper_jwt.rb:

Doorkeeper::JWT.configure do
  # Sign tokens with HS256 using the app's secret key.
  secret_key Rails.application.credentials.secret_key_base
  signing_method :hs256

  token_payload do |opts|
    user = User.find(opts[:resource_owner_id])

    {
      iat: Time.current.to_i,                        # Issued at
      exp: (Time.current + opts[:expires_in]).to_i,  # Expires at
      jti: SecureRandom.uuid,                        # Unique token ID
      sub: user.id,                                  # Subject (user ID)
      email: user.email,
      scopes: opts[:scopes]
    }
  end

  secret_key_path nil
  use_application_secret false
end

Enter fullscreen mode Exit fullscreen mode

If you've never seen a JWT before, here's the short version. A JWT is three base64-encoded parts joined by dots:

header.payload.signature

Enter fullscreen mode Exit fullscreen mode

The header says how the token is signed. The payload is the JSON we just defined above (user ID, email, expiry, etc). The signature is a hash of header.payload combined with our secret key. If an attacker changes anything in the payload, the signature won't match anymore and the token is rejected.

The jti (JWT ID) is worth flagging. It's a unique ID per token, which we'll use later when we implement token revocation. You "blacklist" the jti instead of trying to delete the JWT itself (you can't, the client has it).

Where we are right now

We have a Rails 8 API with a User model, password hashing, OAuth2 password grant, JWT access tokens, refresh tokens, and HttpOnly cookie storage. That's a solid foundation.

But we have not written the controllers yet (register, login, logout, refresh, "who am I"). That's coming in Part 3, along with the first set of mitigations that depend on having endpoints to protect.

Progress tracker: security vectors from Part 1

I'll keep this table updated in every part so you can see exactly what's covered and what's still pending.

# Attack vector Status Where
1 XSS 🟡 Partially mitigated HttpOnly cookie storage (Step 5). Full content sanitization is a frontend concern.
2 SQL Injection 🟢 Mitigated by default Active Record's find_by, where, etc. We'll still review controllers in Part 3.
3 CSRF 🔴 Not yet SameSite=Lax is set, but we need explicit CSRF tokens for cookie auth. Part 3.
4 Brute Force 🟡 Partially mitigated bcrypt slows password cracking. Rate limiting with Rack::Attack comes in Part 4.
5 User Enumeration 🔴 Not yet We need to design login/reset responses to be uniform. Part 3.
6 IDOR 🔴 Not yet Will be addressed when we add resources + Pundit/Sqids. Part 5.
7 Mass Assignment 🔴 Not yet Strong params, controller by controller. Part 3.
8 Excessive Data Exposure 🔴 Not yet Serializers (JSON:API or alba). Part 3.
9 MITM 🟡 Partially mitigated secure: true on cookies in production. force_ssl and HSTS come in Part 4.
10 Token Theft 🟢 Mostly mitigated HttpOnly + encrypted cookies + short-lived tokens + refresh tokens. Revocation in Part 3.
11 Verbose Error Messages 🔴 Not yet Production error handling. Part 4.

Legend: 🟢 Covered, 🟡 Partial, 🔴 Pending

Coming up in Part 3

We'll build the actual auth controllers (register, login, logout, refresh, and me), and while we do it we'll knock out four more vectors from the tracker: CSRF, User Enumeration, Mass Assignment, and Excessive Data Exposure.

If this helped you, follow along so you don't miss it. And if anything is unclear, drop a comment, I read all of them.