<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Renzo Diaz</title>
    <description>The latest articles on Forem by Renzo Diaz (@renzodiaz).</description>
    <link>https://forem.com/renzodiaz</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F284980%2F41b087c6-3041-4b49-a36a-24bc3db6aeae.jpg</url>
      <title>Forem: Renzo Diaz</title>
      <link>https://forem.com/renzodiaz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/renzodiaz"/>
    <language>en</language>
    <item>
      <title>Build a Secure API with Rails 8 - Part-3: Auth Controllers</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Mon, 25 May 2026 00:48:30 +0000</pubDate>
      <link>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-3-auth-controllers-4l2</link>
      <guid>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-3-auth-controllers-4l2</guid>
      <description>&lt;p&gt;Hey folks 👋&lt;/p&gt;

&lt;p&gt;Welcome back. In &lt;a href="https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5"&gt;Part 2&lt;/a&gt; we laid the foundation: a Rails 8 API with a User model, password hashing through Devise, OAuth2 password grant via Doorkeeper, JWT access tokens, refresh tokens, and HttpOnly cookie storage. Solid base, but no actual endpoints yet.&lt;/p&gt;

&lt;p&gt;Today we fix that. We are going to write the 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. We'll also add rate limiting, encrypted DB fields, secure HTTP headers, and structured logging.&lt;/p&gt;

&lt;p&gt;Heads up before we start: this part is longer than Part 2. I thought about splitting it again, but everything here belongs together. Controllers without rate limiting are half-protected, and rate limiting without controllers to protect is pointless. So grab a coffee and let's go.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building in Part 3
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Pundit for authorization, Rack-CORS to control who can talk to our API&lt;/li&gt;
&lt;li&gt;A versioned API structure (&lt;code&gt;/api/v1/...&lt;/code&gt;) so we don't paint ourselves into a corner later&lt;/li&gt;
&lt;li&gt;Auth controllers: register, login, logout, refresh, and a &lt;code&gt;me&lt;/code&gt; endpoint&lt;/li&gt;
&lt;li&gt;Rate limiting with Rack-Attack to slow down brute force attempts&lt;/li&gt;
&lt;li&gt;Encrypted DB fields with Lockbox for sensitive data&lt;/li&gt;
&lt;li&gt;HTTP security headers with secure_headers&lt;/li&gt;
&lt;li&gt;Structured logs with Lograge&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your &lt;a href="https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5"&gt;Part 2&lt;/a&gt; project is sitting on your disk, open it up and let's continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1. Add Pundit and Rack-CORS
&lt;/h2&gt;

&lt;p&gt;Pundit handles authorization (the "what is this user allowed to do" question, as opposed to "who is this user" which Devise already answered). Rack-CORS controls which domains are allowed to make requests to our API from a browser.&lt;/p&gt;

&lt;p&gt;Open your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Authentication &amp;amp; Authorization&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'pundit'&lt;/span&gt;

&lt;span class="c1"&gt;# Security&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rack-cors'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install
&lt;/span&gt;bin/rails g pundit:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The generator creates &lt;code&gt;app/policies/application_policy.rb&lt;/code&gt;, which we'll use later when we add real resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2. Configure CORS
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;config/initializers/cors.rb&lt;/code&gt; and replace it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;insert_before&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cors&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;allow&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c1"&gt;# In production, replace with your real frontend domain&lt;/span&gt;
    &lt;span class="n"&gt;origins&lt;/span&gt; &lt;span class="no"&gt;ENV&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FRONTEND_URL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"http://localhost:5173"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;resource&lt;/span&gt; &lt;span class="s2"&gt;"*"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;headers: :any&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;methods: &lt;/span&gt;&lt;span class="sx"&gt;%i[get post put patch delete options head]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;credentials: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;expose: &lt;/span&gt;&lt;span class="sx"&gt;%w[X-CSRF-Token]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A mistake I have personally made and seen a hundred times in code reviews: don't use &lt;code&gt;origins '*'&lt;/code&gt; with &lt;code&gt;credentials: true&lt;/code&gt;. Browsers will reject it outright, and even if they didn't, it would mean any website on the internet could make authenticated requests to your API. Always pin the origin to your real frontend domain (or domains, you can pass an array).&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;credentials: true&lt;/code&gt; part is required because our auth lives in cookies. Without it, the browser won't attach the session cookie to cross-origin requests, and login will appear to "work" but every following request will look anonymous. I spent an embarrassing afternoon on that one.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: CSRF foundation (Part 1, vector 3)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Locking down origins is the first half of CSRF defense. Combined with &lt;code&gt;SameSite=Lax&lt;/code&gt; from Part 2, a random attacker site can't trick a logged-in user's browser into hitting our API. We'll add explicit CSRF tokens later in this post for the parts that need them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Update ApplicationController
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ApplicationController&lt;/code&gt; is the entry point for every request. Everything else inherits from it. We need it to do three things: include cookie support, plug in Pundit, and require a valid Doorkeeper token by default (so endpoints are private unless we explicitly say otherwise).&lt;/p&gt;

&lt;p&gt;Edit &lt;code&gt;app/controllers/application_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;API&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;ActionController&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cookies&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Pundit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Authorization&lt;/span&gt;

  &lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:doorkeeper_authorize!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unless: :skip_authorization?&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;current_user&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="k"&gt;defined?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;doorkeeper_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doorkeeper_token&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;skip_authorization?&lt;/span&gt;
    &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;skip_authorization?&lt;/code&gt; method defaults to &lt;code&gt;false&lt;/code&gt;, meaning every endpoint requires a token. Individual controllers (like login and register, which obviously can't require you to already be logged in) will override it to return &lt;code&gt;true&lt;/code&gt; for specific actions. I prefer this "deny by default" pattern because forgetting to add auth is a much more common bug than forgetting to mark something public.&lt;/p&gt;

&lt;p&gt;One thing I want to call out: &lt;code&gt;current_user&lt;/code&gt; looks up the user from &lt;code&gt;doorkeeper_token.resource_owner_id&lt;/code&gt;. That &lt;code&gt;resource_owner_id&lt;/code&gt; was set back in Part 2 when Doorkeeper issued the token. The JWT itself carries the user ID, so we are NOT hitting the database to verify the token, only to load the user record. That's the whole point of JWT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4. Create the versioned BaseController
&lt;/h2&gt;

&lt;p&gt;Eventually you will want to release a v2 of your API without breaking the v1 clients that are already in the wild. There are several ways to version an API (custom headers, content negotiation, URL paths). I've used all three and I'll save you the suspense: URL path versioning (&lt;code&gt;/api/v1/...&lt;/code&gt;) is the easiest to debug, the easiest to document, and the easiest for new teammates to understand. We're going with that.&lt;/p&gt;

&lt;p&gt;Create the folder structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; app/controllers/api/v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then create &lt;code&gt;app/controllers/api/v1/base_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Api&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
      &lt;span class="n"&gt;rescue_from&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordNotFound&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;with: :not_found&lt;/span&gt;
      &lt;span class="n"&gt;rescue_from&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;RecordInvalid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;with: :unprocessable_entity&lt;/span&gt;
      &lt;span class="n"&gt;rescue_from&lt;/span&gt; &lt;span class="no"&gt;Pundit&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;NotAuthorizedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;with: :forbidden&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;not_found&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"Not found"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :not_found&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unprocessable_entity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_messages&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
               &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;forbidden&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"Access denied"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :forbidden&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every controller from now on inherits from &lt;code&gt;BaseController&lt;/code&gt;, which means every controller gets these three error handlers for free.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: Verbose Error Messages (Part 1, vector 11)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Notice that &lt;code&gt;forbidden&lt;/code&gt; returns a generic "Access denied" message. It does NOT say "you don't own this record" or "your role is missing the &lt;code&gt;admin&lt;/code&gt; scope". That detail is gold for an attacker probing your API. Same idea for &lt;code&gt;not_found&lt;/code&gt;: we just say "Not found", we don't leak whether the record exists but is private, or doesn't exist at all.&lt;/p&gt;

&lt;p&gt;We'll fully button up production error handling in Part 4, but this is the start.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5. SessionsController (login, logout, refresh)
&lt;/h2&gt;

&lt;p&gt;Now the fun part. The sessions controller handles login, logout, and refresh. I'm going to paste the whole thing and then walk through the parts that matter.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/controllers/api/v1/sessions_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Api&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SessionsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseController&lt;/span&gt;
      &lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:doorkeeper_authorize!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;only: &lt;/span&gt;&lt;span class="sx"&gt;%i[create refresh]&lt;/span&gt;

      &lt;span class="c1"&gt;# POST /api/v1/auth/login&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_for_database_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid_password?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
          &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;set_auth_cookies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;expires_at: &lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:expires_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"Invalid credentials"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# DELETE /api/v1/auth/logout&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;destroy&lt;/span&gt;
        &lt;span class="n"&gt;revoke_tokens&lt;/span&gt;
        &lt;span class="n"&gt;clear_auth_cookies&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Logged out successfully"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="c1"&gt;# POST /api/v1/auth/refresh&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh&lt;/span&gt;
        &lt;span class="n"&gt;refresh_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:refresh_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"No refresh token"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;

        &lt;span class="n"&gt;existing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoked?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;refresh_expired?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="n"&gt;clear_auth_cookies&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"Expired session"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unauthorized&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource_owner_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;existing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;

        &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;set_auth_cookies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="ss"&gt;expires_at: &lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:expires_at&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;skip_authorization?&lt;/span&gt;
        &lt;span class="n"&gt;action_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;in?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%w[create refresh]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;generate_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;resource_owner_id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;expires_in: &lt;/span&gt;&lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;access_token_expires_in&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="s2"&gt;"read write"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;use_refresh_token: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;access_token: &lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;refresh_token: &lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;refresh_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;expires_at: &lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expires_in&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_now&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_auth_cookies&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cookie_opts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;httponly: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;secure: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;same_site: :lax&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cookie_opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;expires: &lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_now&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:refresh_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cookie_opts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="ss"&gt;value: &lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:refresh_token&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
          &lt;span class="ss"&gt;expires: &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_now&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;clear_auth_cookies&lt;/span&gt;
        &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:refresh_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;revoke_tokens&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;by_refresh_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cookies&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:refresh_token&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;revoke&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;refresh_expired?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;days&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth pausing on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The login response is intentionally vague.&lt;/strong&gt; When credentials are wrong, we return &lt;code&gt;"Invalid credentials"&lt;/code&gt;. Not "no user with that email", not "wrong password". Both of those leak information. The first time I built an API I had a friendly "Email not found, would you like to sign up?" message. That message is also a free oracle for an attacker to verify whether &lt;code&gt;victim@gmail.com&lt;/code&gt; has an account on your platform. Don't help them.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: User Enumeration (Part 1, vector 5)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Generic auth errors are the single cheapest mitigation in this whole series. Cost: zero. Benefit: attackers can't build a list of valid emails by hitting your login endpoint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Refresh tokens are rotated.&lt;/strong&gt; Look at the &lt;code&gt;refresh&lt;/code&gt; action: when a refresh token is used, we immediately call &lt;code&gt;existing.revoke&lt;/code&gt; on it before issuing a new one. This means a refresh token is single-use. If an attacker manages to steal a refresh token from your cookies and uses it, the legitimate user's next refresh will fail (because the attacker already burned it), and you can detect the anomaly. We're not implementing the detection part today, but the rotation alone already raises the cost of an attack significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Logout actually revokes the token.&lt;/strong&gt; A surprising number of "logout" endpoints I have reviewed in real production code just delete the cookie. That works for the honest user, but if anyone has copied the JWT before logout, it's still valid until it expires naturally. Calling &lt;code&gt;token.revoke&lt;/code&gt; puts it on the revocation list so it can't be used again, no matter who has it.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: Token Theft (Part 1, vector 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Refresh rotation plus revocation on logout closes the last gap from Part 2. Now if a token leaks, we have a way to kill it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6. RegistrationsController
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;app/controllers/api/v1/registrations_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Api&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegistrationsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseController&lt;/span&gt;
      &lt;span class="n"&gt;skip_before_action&lt;/span&gt; &lt;span class="ss"&gt;:doorkeeper_authorize!&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;
        &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;
          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;message: &lt;/span&gt;&lt;span class="s2"&gt;"Account created successfully"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :created&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;
          &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;errors: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_messages&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :unprocessable_entity&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;skip_authorization?&lt;/span&gt;
        &lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_params&lt;/span&gt;
        &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password_confirmation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting line is &lt;code&gt;user_params&lt;/code&gt;. It explicitly lists which fields are allowed: email, password, password_confirmation. Nothing else. If a clever user POSTs &lt;code&gt;{ "user": { "email": "...", "password": "...", "admin": true, "role": "superuser" } }&lt;/code&gt;, those extra fields are silently dropped.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: Mass Assignment (Part 1, vector 7)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the OWASP "mass assignment" vector. Strong params is Rails' built-in fix. The rule I follow: every controller that takes user input has a &lt;code&gt;*_params&lt;/code&gt; method, and that method whitelists only the fields it expects. Never use &lt;code&gt;params[:user]&lt;/code&gt; directly to build a record. Ever.&lt;/p&gt;

&lt;p&gt;Notice also that on success we don't echo back the user's data. Just a message. That keeps us from accidentally returning fields we never meant to expose.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: Excessive Data Exposure (Part 1, vector 8)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Same idea on the way out. We'll formalize this with serializers later (probably &lt;code&gt;alba&lt;/code&gt; because it's fast and dependency-free), but for now the rule is: build the response hash by hand, listing exactly which fields go out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7. UsersController and the &lt;code&gt;me&lt;/code&gt; endpoint
&lt;/h2&gt;

&lt;p&gt;The frontend needs a way to ask "am I logged in, and if so, who am I?" on page load. That's the classic &lt;code&gt;me&lt;/code&gt; endpoint.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;app/controllers/api/v1/users_controller.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Api&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
    &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UsersController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;BaseController&lt;/span&gt;
      &lt;span class="c1"&gt;# GET /api/v1/me&lt;/span&gt;
      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;me&lt;/span&gt;
        &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;user: &lt;/span&gt;&lt;span class="n"&gt;user_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="ss"&gt;status: :ok&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="kp"&gt;private&lt;/span&gt;

      &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;user_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same pattern as before: hand-built response hash, only the fields we want to expose. &lt;code&gt;encrypted_password&lt;/code&gt;, &lt;code&gt;sign_in_count&lt;/code&gt;, &lt;code&gt;reset_password_token&lt;/code&gt;, none of that should leak out, and with this pattern it can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 8. Wire up the routes
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;config/routes.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;use_doorkeeper&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;skip_controllers&lt;/span&gt; &lt;span class="ss"&gt;:authorizations&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:applications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="ss"&gt;:authorized_applications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:tokens&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:api&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:v1&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;post&lt;/span&gt;   &lt;span class="s1"&gt;'auth/register'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'registrations#create'&lt;/span&gt;
      &lt;span class="n"&gt;post&lt;/span&gt;   &lt;span class="s1"&gt;'auth/login'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'sessions#create'&lt;/span&gt;
      &lt;span class="n"&gt;delete&lt;/span&gt; &lt;span class="s1"&gt;'auth/logout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'sessions#destroy'&lt;/span&gt;
      &lt;span class="n"&gt;post&lt;/span&gt;   &lt;span class="s1"&gt;'auth/refresh'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'sessions#refresh'&lt;/span&gt;

      &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'me'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'users#me'&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"up"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"rails/health#show"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: :rails_health_check&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;skip_controllers&lt;/code&gt; block on Doorkeeper is important. Out of the box, Doorkeeper mounts a bunch of OAuth2 endpoints (authorization page, application management, etc) that are designed for the browser-based OAuth flow. We don't need any of that, so we strip it out. Less code on the public internet means less attack surface.&lt;/p&gt;

&lt;p&gt;Here's the API at a glance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Auth required&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/auth/register&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create new account&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/auth/login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Login, sets cookies&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DELETE&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/auth/logout&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Revoke tokens, clear cookies&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/auth/refresh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Refresh access token&lt;/td&gt;
&lt;td&gt;Cookie only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/api/v1/me&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Current user data&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Step 9. Test with curl
&lt;/h2&gt;

&lt;p&gt;The trick when testing cookie-based auth with curl is the &lt;code&gt;-c&lt;/code&gt; and &lt;code&gt;-b&lt;/code&gt; flags. &lt;code&gt;-c cookies.txt&lt;/code&gt; saves the cookies the server returns, &lt;code&gt;-b cookies.txt&lt;/code&gt; sends them on the next request. It's basically what the browser does for you.&lt;/p&gt;

&lt;p&gt;Register:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/v1/auth/register &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"user": {"email": "me@example.com", "password": "s3cr3tP@ss", "password_confirmation": "s3cr3tP@ss"}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Login (saves cookies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/v1/auth/login &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"email": "me@example.com", "password": "s3cr3tP@ss"}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Me (protected, sends cookies):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/api/v1/me &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Refresh:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/api/v1/auth/refresh &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt &lt;span class="nt"&gt;-c&lt;/span&gt; cookies.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logout:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; DELETE http://localhost:3000/api/v1/auth/logout &lt;span class="nt"&gt;-b&lt;/span&gt; cookies.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If all five of those work, your auth flow is alive. But we're not done. An attacker right now can hit &lt;code&gt;/login&lt;/code&gt; ten thousand times per second with different passwords. Let's fix that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 10. Rate limiting with Rack-Attack
&lt;/h2&gt;

&lt;p&gt;Rack-Attack sits in the middleware stack and inspects every request before it reaches your controllers. It can throttle, block, or safelist IPs based on rules you define. It's stupidly simple to set up and saves you from a lot of pain.&lt;/p&gt;

&lt;p&gt;Add it to your Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Security&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'rack-attack'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;bundle install&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;config/initializers/rack_attack.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Rack::Attack&lt;/span&gt;
  &lt;span class="c1"&gt;# Throttle login attempts by IP: max 5 per 20 seconds&lt;/span&gt;
  &lt;span class="n"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logins/ip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;period: &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'/api/v1/auth/login'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Throttle login attempts by email: max 5 per 20 seconds&lt;/span&gt;
  &lt;span class="n"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'logins/email'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;period: &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'/api/v1/auth/login'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post?&lt;/span&gt;
      &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'email'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;downcase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;gsub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\s+/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# General API limit: 300 requests per IP per 5 minutes&lt;/span&gt;
  &lt;span class="n"&gt;throttle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'api/ip'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;period: &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ip&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;start_with?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/api'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# Custom response when throttled&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throttled_responder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"rack.attack.match_data"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{})[&lt;/span&gt;&lt;span class="ss"&gt;:period&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s1"&gt;'Content-Type'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'Retry-After'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="ss"&gt;error: &lt;/span&gt;&lt;span class="s2"&gt;"Too many requests. Please slow down."&lt;/span&gt; &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why throttle by both IP and email? An attacker with a botnet can rotate through thousands of IPs, and IP-only throttling won't catch them because each IP only sends a few requests. But all those requests are still targeting the same email, so the email-based throttle does catch them. The reverse is also true: a single attacker hammering many different accounts from one IP gets caught by the IP throttle. The two rules together cover both attack shapes.&lt;/p&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: Brute Force (Part 1, vector 4)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Combined with bcrypt's slowness from Part 2, this makes online brute force impractical. Five attempts per 20 seconds means an attacker can try maybe 15 passwords per minute, per email. At that rate, even a weak password takes years to crack.&lt;/p&gt;

&lt;p&gt;One gotcha I learned the hard way: in development, Rack-Attack defaults to using the memory store, which resets every time you restart the server. That's fine. But in production you almost certainly want to point it at Redis, otherwise each app server has its own counter and an attacker can bypass the limit by spreading requests across servers. I'll show that config when we deploy in a later part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 11. HTTP security headers with secure_headers
&lt;/h2&gt;

&lt;p&gt;The browser will enforce a bunch of security policies for you, but only if you tell it to. The &lt;code&gt;secure_headers&lt;/code&gt; gem makes that easy.&lt;/p&gt;

&lt;p&gt;Add it to the Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'secure_headers'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bundle install&lt;/code&gt;, then create &lt;code&gt;config/initializers/secure_headers.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;SecureHeaders&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# Force HTTPS for 1 year&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hsts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"max-age=31536000; includeSubDomains"&lt;/span&gt;

  &lt;span class="c1"&gt;# Restrict what resources can load&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;csp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="ss"&gt;default_src: &lt;/span&gt;&lt;span class="sx"&gt;%w['self']&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;script_src: &lt;/span&gt;&lt;span class="sx"&gt;%w['self']&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="ss"&gt;connect_src: &lt;/span&gt;&lt;span class="sx"&gt;%w['self']&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;# Prevent your site from being framed (clickjacking)&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x_frame_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"DENY"&lt;/span&gt;

  &lt;span class="c1"&gt;# Stop MIME type sniffing&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x_content_type_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"nosniff"&lt;/span&gt;

  &lt;span class="c1"&gt;# Legacy XSS filter (still useful for older browsers)&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;x_xss_protection&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"1; mode=block"&lt;/span&gt;

  &lt;span class="c1"&gt;# Don't leak full URLs in the Referer header&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;referrer_policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"strict-origin-when-cross-origin"&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Quick tour of what each header does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt; tells browsers "for the next year, never talk to this domain over plain HTTP". This shuts down SSL stripping attacks, where an attacker on the same Wi-Fi downgrades your connection to HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Frame-Options: DENY&lt;/strong&gt; stops other sites from loading yours in an iframe, which is the foundation of clickjacking attacks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;X-Content-Type-Options: nosniff&lt;/strong&gt; stops browsers from guessing the content type of a response, which has historically been used to execute scripts that were uploaded as "images".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSP&lt;/strong&gt; is the big one. It tells the browser exactly which sources of scripts, styles, and connections are allowed. Our config here is very strict ("self" only), which is appropriate for a pure API. If you're serving HTML too, you'll need to relax it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🛡️ &lt;strong&gt;Mitigation in action: MITM (Part 1, vector 9)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;HSTS plus &lt;code&gt;secure: true&lt;/code&gt; on cookies (set in Part 2) is the one-two punch against MITM attacks. Combined with &lt;code&gt;force_ssl&lt;/code&gt; in production (coming in Part 4), there's no way for an attacker on your network to downgrade the connection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 12. Encrypt sensitive fields with Lockbox
&lt;/h2&gt;

&lt;p&gt;bcrypt protects passwords because it's a one-way hash, you never need to read the original. But what about fields you DO need to read, like phone numbers, addresses, or government IDs? You can't hash those. You need to encrypt them, so the database stores ciphertext and the app decrypts it in memory when needed.&lt;/p&gt;

&lt;p&gt;That's what Lockbox does.&lt;/p&gt;

&lt;p&gt;Add to the Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Encryption&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'lockbox'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bundle install&lt;/code&gt;. Then generate a master key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails runner &lt;span class="s2"&gt;"puts Lockbox.generate_key"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Take the output and store it in your Rails credentials (&lt;code&gt;bin/rails credentials:edit&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;lockbox_master_key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;&amp;lt;paste the generated key here&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;config/initializers/lockbox.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Lockbox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;master_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lockbox_master_key&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now in any model, you can mark fields as encrypted. For example, if we added a phone column to User:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;User&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="n"&gt;encrypts&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;

  &lt;span class="c1"&gt;# If you need to search by encrypted field, use blind_index&lt;/span&gt;
  &lt;span class="n"&gt;blind_index&lt;/span&gt; &lt;span class="ss"&gt;:phone&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The migration would look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate migration AddEncryptedPhoneToUsers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:phone_ciphertext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:text&lt;/span&gt;
&lt;span class="n"&gt;add_column&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:phone_bidx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:text&lt;/span&gt;  &lt;span class="c1"&gt;# for blind_index searching&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;bin/rails db:migrate&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;encrypts :phone&lt;/code&gt; means the value is encrypted before INSERT and decrypted on read. The database column is literally named &lt;code&gt;phone_ciphertext&lt;/code&gt; and contains base64 ciphertext. If someone dumps your database, they see nothing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;blind_index&lt;/code&gt; is a clever workaround for the obvious problem with encrypted fields: you can't search them, because two encryptions of the same value produce different ciphertext. The blind index is a deterministic hash of the value, stored separately, which you can search on. It's not as private as the encryption itself, but it lets &lt;code&gt;User.where(phone: "...")&lt;/code&gt; actually find records.&lt;/p&gt;

&lt;p&gt;I'm not adding a phone field in this tutorial because we don't need one yet, but I wanted to show the pattern so you can apply it when you do add sensitive fields. Personal rule of thumb: any field that would be embarrassing or harmful if it leaked, encrypt it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 13. Structured logs with Lograge
&lt;/h2&gt;

&lt;p&gt;Rails' default log format is fine for development but painful in production. You get six lines per request, none of them parseable as structured data, and no consistent fields. Lograge condenses each request to a single structured line that you can ship to Datadog, Loki, CloudWatch, or whatever you use.&lt;/p&gt;

&lt;p&gt;Add to the Gemfile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Monitoring &amp;amp; Logging&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'lograge'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;bundle install&lt;/code&gt;. Then create &lt;code&gt;config/initializers/lograge.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lograge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;enabled&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lograge&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;custom_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;lambda&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;time: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="ss"&gt;user_id: &lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To get &lt;code&gt;user_id&lt;/code&gt; into logs, you'll want to append it to the payload from &lt;code&gt;ApplicationController&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;append_info_to_payload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:host&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host&lt;/span&gt;
  &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:user_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;doorkeeper_token&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is small but huge when you're debugging at 2 AM and trying to figure out which user's session caused that 500 error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we are right now
&lt;/h2&gt;

&lt;p&gt;Five working auth endpoints, rate limiting, encrypted DB fields ready when you need them, hardened HTTP headers, and structured logs. The API is genuinely usable now, not just configured.&lt;/p&gt;

&lt;p&gt;But we still have gaps. Most notably, we haven't done explicit CSRF tokens yet (the SameSite cookie helps but isn't enough on its own for state-changing endpoints), and we haven't tackled IDOR (insecure direct object references) which only becomes a problem once we have resources to reference. Both of those plus production error handling, &lt;code&gt;force_ssl&lt;/code&gt;, and serializers are coming up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress tracker: security vectors from Part 1
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Attack vector&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Where&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;XSS&lt;/td&gt;
&lt;td&gt;🟢 Mostly mitigated&lt;/td&gt;
&lt;td&gt;HttpOnly cookies (Part 2) + strict CSP headers (Step 11)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;SQL Injection&lt;/td&gt;
&lt;td&gt;🟢 Mitigated by default&lt;/td&gt;
&lt;td&gt;Active Record + strong params throughout controllers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;CSRF&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;SameSite cookies + pinned CORS origins. Explicit CSRF tokens still pending. Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Brute Force&lt;/td&gt;
&lt;td&gt;🟢 Mitigated&lt;/td&gt;
&lt;td&gt;bcrypt + Rack-Attack IP and email throttles (Step 10)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;User Enumeration&lt;/td&gt;
&lt;td&gt;🟢 Mitigated&lt;/td&gt;
&lt;td&gt;Generic "Invalid credentials" message (Step 5)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;IDOR&lt;/td&gt;
&lt;td&gt;🔴 Not yet&lt;/td&gt;
&lt;td&gt;Will be addressed with Pundit policies + Sqids. Part 5.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Mass Assignment&lt;/td&gt;
&lt;td&gt;🟢 Mitigated&lt;/td&gt;
&lt;td&gt;Strong params in every controller (Step 6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Excessive Data Exposure&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;Hand-built response hashes. Formal serializers in Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;MITM&lt;/td&gt;
&lt;td&gt;🟢 Mostly mitigated&lt;/td&gt;
&lt;td&gt;HSTS (Step 11) + secure cookies. &lt;code&gt;force_ssl&lt;/code&gt; in Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Token Theft&lt;/td&gt;
&lt;td&gt;🟢 Mitigated&lt;/td&gt;
&lt;td&gt;HttpOnly + encrypted cookies + short tokens + rotation + revocation on logout&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Verbose Error Messages&lt;/td&gt;
&lt;td&gt;🟡 Partially mitigated&lt;/td&gt;
&lt;td&gt;Generic 403/404 responses (Step 4). Production rescue handler in Part 4.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Legend: 🟢 Covered, 🟡 Partial, 🔴 Pending&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming up in Part 4
&lt;/h2&gt;

&lt;p&gt;We finish the security checklist. CSRF tokens for the endpoints that need them, &lt;code&gt;force_ssl&lt;/code&gt; and the production error handler, and we'll introduce a serializer library so we stop hand-rolling response hashes in every action. After that we'll be ready to start adding actual resources (and the IDOR mitigation that comes with them) in Part 5.&lt;/p&gt;

&lt;p&gt;If this helped, follow along so you catch the next one. And if anything broke or didn't make sense, drop a comment, I read all of them.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>api</category>
      <category>owasp</category>
    </item>
    <item>
      <title>Build a Secure API with Rails 8 - Part-2: Authentication Foundations</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 13 May 2026 16:44:18 +0000</pubDate>
      <link>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5</link>
      <guid>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-2-authentication-foundations-2fo5</guid>
      <description>&lt;p&gt;Hey folks 👋&lt;/p&gt;

&lt;p&gt;Welcome back. In &lt;a href="https://dev.to/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh"&gt;Part 1&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead of &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;To help us stay oriented, I'll keep a small progress tracker at the end of each post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are building in Part 2
&lt;/h2&gt;

&lt;p&gt;A small Rails 8 API with:&lt;/p&gt;

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

&lt;p&gt;If you've never touched Devise or Doorkeeper before, don't worry. I'll explain &lt;em&gt;why&lt;/em&gt; we use each piece, not just &lt;em&gt;how&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ruby on Rails 8&lt;/li&gt;
&lt;li&gt;PostgreSQL 14+&lt;/li&gt;
&lt;li&gt;Postman, Insomnia, or &lt;code&gt;curl&lt;/code&gt; for testing&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1. Create the project in API mode
&lt;/h2&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new secure_api_auth &lt;span class="nt"&gt;-T&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; postgresql &lt;span class="nt"&gt;--api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking that down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-T&lt;/code&gt; skips the default test suite (we'll set up testing later in the series)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-d postgresql&lt;/code&gt; uses Postgres instead of SQLite&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--api&lt;/code&gt; strips out browser-oriented middleware&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;secure_api_auth
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip from experience:&lt;/strong&gt; commit right here as "Initial commit" before you change anything. When something breaks two hours from now, having a clean baseline to &lt;code&gt;git diff&lt;/code&gt; against will save you.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 2. Add the gems we need
&lt;/h2&gt;

&lt;p&gt;Open your &lt;code&gt;Gemfile&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Database&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'pg'&lt;/span&gt;

&lt;span class="c1"&gt;# Password hashing&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'bcrypt'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'~&amp;gt; 3.1.7'&lt;/span&gt;

&lt;span class="c1"&gt;# Authentication &amp;amp; Authorization&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'devise'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'doorkeeper'&lt;/span&gt;
&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'doorkeeper-jwt'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bundle &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick mental model before we go further, because these three gems confused me for a long time:&lt;/p&gt;

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

&lt;p&gt;In other words: Devise checks the ID at the door, Doorkeeper hands you the wristband, and JWT is what the wristband is made of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3. Install Devise
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate devise:install
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;config/environments/development.rb&lt;/code&gt; and add:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;action_mailer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;default_url_options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;host: &lt;/span&gt;&lt;span class="s1"&gt;'localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;port: &lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We won't be sending real emails in this tutorial, but Devise complains if this isn't set.&lt;/p&gt;

&lt;h3&gt;
  
  
  Generate the User model
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate devise User
bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;users&lt;/code&gt; table with an &lt;code&gt;encrypted_password&lt;/code&gt; column (and a few others for tracking sign-in counts and lockouts).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: Token Theft and Password Breaches (Part 1, vectors 4 and 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Devise hashes passwords with &lt;strong&gt;bcrypt&lt;/strong&gt;, 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.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Switch Devise to API mode
&lt;/h3&gt;

&lt;p&gt;By default Devise wants to redirect users to HTML pages after login. We need to turn that off.&lt;/p&gt;

&lt;p&gt;Open &lt;code&gt;config/initializers/devise.rb&lt;/code&gt; and add (or modify) these two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Devise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setup&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="c1"&gt;# ... keep everything else as generated ...&lt;/span&gt;

  &lt;span class="c1"&gt;# Don't use session storage for API authentication&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;skip_session_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:http_auth&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:params_auth&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

  &lt;span class="c1"&gt;# No HTML redirects, we return JSON&lt;/span&gt;
  &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;navigational_formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4. Install Doorkeeper
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails generate doorkeeper:install
bin/rails generate doorkeeper:migration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;Open the generated migration file (something like &lt;code&gt;db/migrate/XXXXX_create_doorkeeper_tables.rb&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In the oauth_applications table: allow null redirect_uri&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt; &lt;span class="ss"&gt;:redirect_uri&lt;/span&gt;  &lt;span class="c1"&gt;# remove the `null: false`&lt;/span&gt;

&lt;span class="c1"&gt;# In the oauth_access_tokens table: allow null application reference&lt;/span&gt;
&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:application&lt;/span&gt;  &lt;span class="c1"&gt;# remove the `null: false`&lt;/span&gt;

&lt;span class="c1"&gt;# At the bottom of the file, link tokens back to users&lt;/span&gt;
&lt;span class="n"&gt;add_foreign_key&lt;/span&gt; &lt;span class="ss"&gt;:oauth_access_grants&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: :resource_owner_id&lt;/span&gt;
&lt;span class="n"&gt;add_foreign_key&lt;/span&gt; &lt;span class="ss"&gt;:oauth_access_tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;column: :resource_owner_id&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then migrate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;bin/rails db:migrate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A word on the password grant flow.&lt;/strong&gt; 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 &lt;strong&gt;first-party&lt;/strong&gt; 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 &lt;em&gt;first-party&lt;/em&gt;: you control both ends.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 5. Re-enable cookies in API mode
&lt;/h2&gt;

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

&lt;p&gt;Open &lt;code&gt;config/application.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;SecureApiAuth&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Application&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Application&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load_defaults&lt;/span&gt; &lt;span class="mf"&gt;8.0&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;api_only&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

    &lt;span class="c1"&gt;# Re-add cookie middleware so we can use HttpOnly cookie auth&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Cookies&lt;/span&gt;
    &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;middleware&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt; &lt;span class="no"&gt;ActionDispatch&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Session&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;CookieStore&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;key: &lt;/span&gt;&lt;span class="s1"&gt;'_secure_api_session'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;same_site: :lax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                          &lt;span class="ss"&gt;secure: &lt;/span&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;production?&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: XSS and Token Theft (Part 1, vectors 1 and 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the single most important decision in this whole post. If you store a JWT in &lt;code&gt;localStorage&lt;/code&gt;, 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.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 6. Teach Doorkeeper to read tokens from the cookie
&lt;/h2&gt;

&lt;p&gt;Out of the box, Doorkeeper looks for tokens in the &lt;code&gt;Authorization: Bearer ...&lt;/code&gt; header. We need to add a new place for it to look: our encrypted cookie.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;lib/doorkeeper/from_cookie.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Doorkeeper&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;FromCookie&lt;/span&gt;
    &lt;span class="kp"&gt;module_function&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="c1"&gt;# Read the encrypted access token from the HttpOnly cookie&lt;/span&gt;
      &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cookie_jar&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypted&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:access_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;encrypted&lt;/code&gt; and not just &lt;code&gt;signed&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

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

&lt;h2&gt;
  
  
  Step 7. Configure Doorkeeper
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;config/initializers/doorkeeper.rb&lt;/code&gt; and replace the generated content with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# frozen_string_literal: true&lt;/span&gt;

&lt;span class="nb"&gt;require_relative&lt;/span&gt; &lt;span class="s1"&gt;'../../lib/doorkeeper/from_cookie'&lt;/span&gt;

&lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;orm&lt;/span&gt; &lt;span class="ss"&gt;:active_record&lt;/span&gt;
  &lt;span class="n"&gt;api_only&lt;/span&gt;

  &lt;span class="c1"&gt;# Resource Owner Password Credentials grant.&lt;/span&gt;
  &lt;span class="c1"&gt;# Lets users log in with email + password directly.&lt;/span&gt;
  &lt;span class="n"&gt;grant_flows&lt;/span&gt; &lt;span class="sx"&gt;%w[password]&lt;/span&gt;
  &lt;span class="n"&gt;allow_blank_redirect_uri&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# How to find the user when they log in.&lt;/span&gt;
  &lt;span class="n"&gt;resource_owner_from_credentials&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;_routes&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find_for_database_authentication&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;valid_password?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;skip_client_authentication_for_password_grant&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

  &lt;span class="c1"&gt;# Short-lived access tokens.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_expires_in&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;minutes&lt;/span&gt;

  &lt;span class="c1"&gt;# Enable refresh tokens.&lt;/span&gt;
  &lt;span class="n"&gt;use_refresh_token&lt;/span&gt;

  &lt;span class="c1"&gt;# Use JWT format for access tokens.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_generator&lt;/span&gt; &lt;span class="s1"&gt;'Doorkeeper::JWT'&lt;/span&gt;

  &lt;span class="c1"&gt;# Scopes. We'll use these later for authorization.&lt;/span&gt;
  &lt;span class="n"&gt;default_scopes&lt;/span&gt; &lt;span class="ss"&gt;:read&lt;/span&gt;
  &lt;span class="n"&gt;optional_scopes&lt;/span&gt; &lt;span class="ss"&gt;:write&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:admin&lt;/span&gt;

  &lt;span class="n"&gt;base_controller&lt;/span&gt; &lt;span class="s1"&gt;'ApplicationController'&lt;/span&gt;

  &lt;span class="c1"&gt;# Token lookup order: 1) our cookie, 2) Bearer header, 3) query param.&lt;/span&gt;
  &lt;span class="n"&gt;access_token_methods&lt;/span&gt; &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;FromCookie&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="ss"&gt;:from_bearer_authorization&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="ss"&gt;:from_access_token_param&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few of these settings deserve a closer look:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;access_token_expires_in 15.minutes&lt;/code&gt;&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;use_refresh_token&lt;/code&gt;&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;access_token_methods&lt;/code&gt;&lt;/strong&gt; order matters here. Doorkeeper checks the cookie first, then the &lt;code&gt;Authorization&lt;/code&gt; header, then a query parameter. In production I'd actually remove &lt;code&gt;from_access_token_param&lt;/code&gt; 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.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🛡️ Mitigation in action: Token Theft (Part 1, vector 10)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;/logout&lt;/code&gt; endpoint.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 8. Configure the JWT payload
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;config/initializers/doorkeeper_jwt.rb&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;JWT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Sign tokens with HS256 using the app's secret key.&lt;/span&gt;
  &lt;span class="n"&gt;secret_key&lt;/span&gt; &lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secret_key_base&lt;/span&gt;
  &lt;span class="n"&gt;signing_method&lt;/span&gt; &lt;span class="ss"&gt;:hs256&lt;/span&gt;

  &lt;span class="n"&gt;token_payload&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:resource_owner_id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="ss"&gt;iat: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Issued at&lt;/span&gt;
      &lt;span class="ss"&gt;exp: &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;current&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:expires_in&lt;/span&gt;&lt;span class="p"&gt;]).&lt;/span&gt;&lt;span class="nf"&gt;to_i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# Expires at&lt;/span&gt;
      &lt;span class="ss"&gt;jti: &lt;/span&gt;&lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uuid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                        &lt;span class="c1"&gt;# Unique token ID&lt;/span&gt;
      &lt;span class="ss"&gt;sub: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                                  &lt;span class="c1"&gt;# Subject (user ID)&lt;/span&gt;
      &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="n"&gt;opts&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:scopes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;secret_key_path&lt;/span&gt; &lt;span class="kp"&gt;nil&lt;/span&gt;
  &lt;span class="n"&gt;use_application_secret&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've never seen a JWT before, here's the short version. A JWT is three base64-encoded parts joined by dots:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;header.payload.signature
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 &lt;code&gt;header.payload&lt;/code&gt; combined with our secret key. If an attacker changes anything in the payload, the signature won't match anymore and the token is rejected.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;jti&lt;/code&gt; (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 &lt;code&gt;jti&lt;/code&gt; instead of trying to delete the JWT itself (you can't, the client has it).&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we are right now
&lt;/h2&gt;

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

&lt;p&gt;But we have &lt;strong&gt;not&lt;/strong&gt; 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.&lt;/p&gt;

&lt;h2&gt;
  
  
  Progress tracker: security vectors from Part 1
&lt;/h2&gt;

&lt;p&gt;I'll keep this table updated in every part so you can see exactly what's covered and what's still pending.&lt;/p&gt;

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

&lt;p&gt;Legend: 🟢 Covered, 🟡 Partial, 🔴 Pending&lt;/p&gt;

&lt;h2&gt;
  
  
  Coming up in Part 3
&lt;/h2&gt;

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

&lt;p&gt;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.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>ruby</category>
      <category>api</category>
      <category>owasp</category>
    </item>
    <item>
      <title>Build a Secure API with Rails 8 - Part-1</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 06 May 2026 23:14:19 +0000</pubDate>
      <link>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh</link>
      <guid>https://forem.com/renzodiaz/build-a-secure-api-with-rails-8-part-1-11lh</guid>
      <description>&lt;p&gt;Hi folks👋! &lt;/p&gt;

&lt;p&gt;In this post I want to share something I wish I had when I started building APIs with Ruby on Rails: a practical guide that takes security seriously from the beginning.&lt;/p&gt;

&lt;p&gt;When I built my first REST API, most tutorials I found were focused on getting something running quickly. They were great for learning the basics, but they usually skipped important topics like API versioning, authentication strategy, authorization, and security.&lt;/p&gt;

&lt;p&gt;Even when using AI tools to generate a “secure API”, the result is often still insecure unless you already understand the threats you are trying to protect against. Security is not something you get automatically. You need to know what problems you are solving and why the protections matter.&lt;/p&gt;

&lt;p&gt;I ended up reading API design books, OWASP documentation, and real-world breach reports before I finally felt like I understood what I was building, I've put all in practice. This post is the guide I wish I had back then.&lt;/p&gt;

&lt;p&gt;In this series we are going to build a production-ready Rails 8 API with authentication, authorization, rate limiting, secure cookies, security headers, and other important protections. I also want to explain the reasoning behind each decision, not just copy-paste code without context.&lt;/p&gt;

&lt;p&gt;Before writing any code, let’s first understand the main attack vectors we need to defend against.&lt;/p&gt;

&lt;h2&gt;
  
  
  The attack vectors we are defending against
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. XSS (Cross-Site Scripting)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
XSS happens when an attacker injects malicious JavaScript into content that later gets rendered in another user’s browser. In API-driven applications, one of the biggest risks is token theft. If JWTs are stored in &lt;code&gt;localStorage&lt;/code&gt;, a malicious script can read and steal them immediately.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Avoid storing authentication tokens in &lt;code&gt;localStorage&lt;/code&gt; or other browser-accessible storage. Instead, store them in secure &lt;code&gt;HttpOnly&lt;/code&gt; cookies so JavaScript cannot access them. Cookies should also use the &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;SameSite&lt;/code&gt; attributes. Any user-generated content rendered in the frontend should be properly escaped or sanitized.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. SQL Injection
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
SQL Injection happens when user input is inserted directly into a SQL query without proper sanitization. An attacker can manipulate the query to bypass authentication, read sensitive data, or modify the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Avoid interpolating user input directly into SQL queries. In Rails, prefer Active Record methods like &lt;code&gt;where&lt;/code&gt;, &lt;code&gt;find_by&lt;/code&gt;, and parameterized queries, which automatically sanitize input. If raw SQL is unavoidable, use bound parameters instead of string interpolation. You should also validate input, use strong parameters, and follow the principle of least privilege for database accounts.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. CSRF (Cross-Site Request Forgery)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
CSRF happens when a malicious website tricks a logged-in user’s browser into sending authenticated requests to your application using automatically attached cookies.&lt;/p&gt;

&lt;p&gt;This is especially important in Rails APIs using session cookies or JWTs stored in &lt;code&gt;HttpOnly&lt;/code&gt; cookies. Even though JavaScript cannot read those cookies, the browser still sends them automatically with requests.&lt;/p&gt;

&lt;p&gt;An attacker could potentially trigger actions like changing account settings, creating resources, or deleting data without the user realizing it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Enable CSRF protection for any cookie-based authentication flow. In Rails, use &lt;code&gt;protect_from_forgery&lt;/code&gt; and require valid CSRF tokens for state-changing requests like &lt;code&gt;POST&lt;/code&gt;, &lt;code&gt;PUT&lt;/code&gt;, &lt;code&gt;PATCH&lt;/code&gt;, and &lt;code&gt;DELETE&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Authentication cookies should also use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;HttpOnly&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Secure&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;SameSite=Lax&lt;/code&gt; or &lt;code&gt;SameSite=Strict&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also validate &lt;code&gt;Origin&lt;/code&gt; and &lt;code&gt;Referer&lt;/code&gt; headers and keep CORS restricted to trusted frontend domains.&lt;/p&gt;

&lt;p&gt;If the browser automatically sends authentication, CSRF protection still matters, even if the API itself is technically stateless.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. Brute Force
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Brute force attacks happen when an attacker repeatedly tries large numbers of username and password combinations against your login endpoint.&lt;/p&gt;

&lt;p&gt;This commonly targets login forms, password reset endpoints, and authentication APIs. Successful attacks can lead to account compromise, credential stuffing, and unnecessary server load.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Use rate limiting on authentication-related endpoints. In Rails, tools like &lt;code&gt;Rack::Attack&lt;/code&gt; can throttle repeated requests by IP address, email, or both.&lt;/p&gt;

&lt;p&gt;You should also:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;temporarily lock accounts after repeated failures&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;require strong passwords&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;detect suspicious login activity&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;avoid revealing whether an account exists&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;consider CAPTCHA or step-up verification after suspicious behavior&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  5. User Enumeration
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
User enumeration happens when an application reveals whether an account exists through different error messages.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Email not found”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“Incorrect password”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An attacker can use these differences to discover valid accounts and later target them with brute force attacks, phishing, or credential stuffing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Return consistent responses during login, password reset, and account recovery flows.&lt;/p&gt;

&lt;p&gt;Instead of exposing whether the email exists, use generic responses such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;“Invalid credentials”&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;“If an account exists, instructions have been sent”&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also rate limit these endpoints and monitor repeated probing attempts.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. IDOR (Insecure Direct Object Reference)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
IDOR happens when users can access resources they do not own by changing identifiers in URLs or request parameters.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If ownership checks are missing, changing &lt;code&gt;/users/42&lt;/code&gt; to &lt;code&gt;/users/43&lt;/code&gt; could expose another user’s data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Always scope records through the authenticated user or an authorization policy.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Prefer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;posts&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Authorization libraries like Pundit or CanCanCan also help enforce access rules consistently across the application. I also avoid exposing raw database IDs directly to the frontend. Instead, I use &lt;a href="https://sqids.org/ruby" rel="noopener noreferrer"&gt;Sqids&lt;/a&gt;￼to generate less predictable public IDs, which helps reduce simple enumeration attacks.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Mass Assignment
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Mass assignment happens when the application accepts user input and blindly assigns it to model attributes.&lt;/p&gt;

&lt;p&gt;An attacker could submit unexpected fields such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;

  &lt;/span&gt;&lt;span class="nl"&gt;"admin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those fields are not filtered properly, the attacker may gain elevated privileges or modify protected data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Use strong parameters in every controller.&lt;/p&gt;

&lt;p&gt;In Rails, always whitelist allowed attributes using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never pass raw &lt;code&gt;params&lt;/code&gt; directly into &lt;code&gt;create&lt;/code&gt; or &lt;code&gt;update&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sensitive fields like roles, permissions, ownership fields, or account status flags should never be user-assignable.&lt;/p&gt;

&lt;h3&gt;
  
  
  8. Excessive Data Exposure
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Excessive data exposure happens when an API returns more information than the client actually needs.&lt;/p&gt;

&lt;p&gt;This often happens when entire Active Record objects are rendered directly into JSON responses.&lt;/p&gt;

&lt;p&gt;Sensitive data such as password digests, internal IDs, permissions, API keys, or private metadata may accidentally leak through the API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Only return the fields the client actually needs.&lt;/p&gt;

&lt;p&gt;Instead of blindly rendering full objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="ss"&gt;json: &lt;/span&gt;&lt;span class="vi"&gt;@user&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use serializers or custom JSON responses that explicitly define safe attributes.&lt;/p&gt;

&lt;p&gt;Sensitive fields should never appear in API responses.&lt;/p&gt;

&lt;p&gt;You should also regularly review serialized responses to make sure no internal data is leaking unintentionally.&lt;/p&gt;

&lt;h3&gt;
  
  
  9. MITM (Man-in-the-Middle)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
A Man-in-the-Middle attack happens when an attacker intercepts traffic between the client and server.&lt;/p&gt;

&lt;p&gt;Without HTTPS, credentials, tokens, cookies, and other sensitive data can travel in plain text and be stolen or modified.&lt;/p&gt;

&lt;p&gt;Attackers on the same network, malicious proxies, or compromised routers can hijack sessions or impersonate users.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Always enforce HTTPS.&lt;/p&gt;

&lt;p&gt;In Rails, enable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;
&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;force_ssl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This redirects insecure requests and ensures cookies are only sent over encrypted connections.&lt;/p&gt;

&lt;p&gt;Authentication cookies should also use the &lt;code&gt;Secure&lt;/code&gt; and &lt;code&gt;HttpOnly&lt;/code&gt; flags.&lt;/p&gt;

&lt;p&gt;You should additionally enable HSTS headers and avoid loading insecure mixed-content resources.&lt;/p&gt;

&lt;h3&gt;
  
  
  10. Token Theft
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Token theft happens when an attacker gains access to a valid authentication token and uses it to impersonate a user.&lt;/p&gt;

&lt;p&gt;Stolen JWTs can come from XSS attacks, insecure storage, leaked logs, browser extensions, compromised devices, or intercepted traffic.&lt;/p&gt;

&lt;p&gt;If tokens remain valid for a long time, the attacker may keep access even after the user notices something is wrong.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Reduce token exposure and keep token lifetimes short.&lt;/p&gt;

&lt;p&gt;Prefer storing tokens in secure &lt;code&gt;HttpOnly&lt;/code&gt; cookies instead of &lt;code&gt;localStorage&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Use:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;short-lived access tokens&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;refresh token rotation&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;token revocation mechanisms&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You should also avoid exposing tokens in logs or URLs and protect the application against XSS vulnerabilities.&lt;/p&gt;

&lt;h3&gt;
  
  
  11. Verbose Error Messages
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;🚨 Threat:&lt;/strong&gt;&lt;br&gt;
Verbose error messages expose internal application details to attackers.&lt;/p&gt;

&lt;p&gt;Stack traces, database errors, framework versions, SQL queries, and file paths can all help attackers understand how the system works and make exploitation easier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;🛡️ Mitigation:&lt;/strong&gt;&lt;br&gt;
Production applications should return generic and safe error responses.&lt;/p&gt;

&lt;p&gt;Instead of exposing internal exceptions, return messages such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Internal Server Error&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;Invalid request&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Detailed errors should only be logged internally for debugging.&lt;/p&gt;

&lt;p&gt;In Rails, make sure debug pages and detailed exceptions are disabled in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;These are some of the most important security risks to think about when building APIs, and we will revisit them throughout this series as we implement each feature step by step.&lt;/p&gt;

&lt;p&gt;In Part 2 we will start building the Rails 8 API from scratch and set up the project foundation correctly from the beginning, including authentication, secure configuration, and API structure.&lt;/p&gt;

&lt;p&gt;Follow along if you want to get notified when the next part is published.&lt;/p&gt;

</description>
      <category>rails</category>
      <category>api</category>
      <category>ruby</category>
      <category>owasp</category>
    </item>
    <item>
      <title>What are my day-to-day tools as a full-stack developer?</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Sat, 20 Jun 2020 00:26:50 +0000</pubDate>
      <link>https://forem.com/renzodiaz/what-are-my-day-to-day-tools-as-a-full-stack-developer-1lan</link>
      <guid>https://forem.com/renzodiaz/what-are-my-day-to-day-tools-as-a-full-stack-developer-1lan</guid>
      <description>&lt;p&gt;I've divided a list of tools that I use every day working as a full-stack developer by category.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Graphic &lt;a href="https://www.graphic.com/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;I can't afford Illustrator monthly so I found this alternative, I paid once and it worth all. I can edit vectors, AI files, export everything that you can do with Illustrator.&lt;/p&gt;

&lt;h4&gt;
  
  
  SketchApp &lt;a href="https://www.sketch.com/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;When it comes to design mockups I use Sketch, it has a friendly interface that makes it easy to design.&lt;/p&gt;

&lt;h4&gt;
  
  
  Zeplin &lt;a href="https://zeplin.io/"&gt;link&lt;/a&gt;:
&lt;/h4&gt;

&lt;p&gt;If you are developing an application you should have quick access to the mockup assets like images, colors, icons, etc. Zeplin is a design collaboration tool that allows uploading your mockups to the cloud you all the development team can access them quickly and easily.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Iterm2 &lt;a href="https://www.iterm2.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;An alternative to default terminal on macOS&lt;/p&gt;

&lt;h4&gt;
  
  
  Vim &lt;a href="https://www.vim.org/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;I use Vim as the code editor, I've customized shortcuts and plugins for fast development. &lt;/p&gt;

&lt;h4&gt;
  
  
  Docker &lt;a href="https://www.docker.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Docker to containerize my apps&lt;/p&gt;

&lt;h4&gt;
  
  
  Xcode&lt;a href="https://developer.apple.com/xcode/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I need to develop an IOS app I use Xcode with Swift.&lt;/p&gt;

&lt;h4&gt;
  
  
  TMUX &lt;a href="https://github.com/tmux/tmux/wiki"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I'm developing a web application I need quick access to different terminals, for example, to run a server, to use my Vim editor all in just one terminal in a single session.&lt;/p&gt;

&lt;h4&gt;
  
  
  Homebrew &lt;a href="https://brew.sh/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;A package manager for macOS.&lt;/p&gt;

&lt;h4&gt;
  
  
  Repl &lt;a href="https://repl.it"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Sometimes I need to write quick methods to see what returns on the console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Services
&lt;/h2&gt;

&lt;h4&gt;
  
  
  Gitlab &lt;a href="https://gitlab.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;I'm a fan of Gitlab, private repositories, CI/CD and you can also use the community edition to install in your own server.&lt;/p&gt;

&lt;h4&gt;
  
  
  HEROKU &lt;a href="https://heroku.com"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Where I deploy my personal projects.&lt;/p&gt;

&lt;h4&gt;
  
  
  AWS S3 &lt;a href="https://aws.amazon.com/s3/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;When I'm working on a big application I store the resources in a bucket in AWS S3, and when I need to create a CDN as well.&lt;/p&gt;

&lt;h4&gt;
  
  
  PivotalTracker &lt;a href="https://www.pivotaltracker.com/"&gt;link&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Every developer should use a tool to track progress on the development or specific task, I use PivotalTracker for big projects and Gitlab board for small.&lt;/p&gt;

&lt;h2&gt;
  
  
  Utilities
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;1Password: My generate and store passwords.&lt;/li&gt;
&lt;li&gt;Boostnote: Save my snippets or documentation.&lt;/li&gt;
&lt;li&gt;Workspace: I organize my apps in workspaces, so I can access them in one click.&lt;/li&gt;
&lt;li&gt;Toggle Alfred: Enhanced spotlight in mac.&lt;/li&gt;
&lt;li&gt;ExpressVPN: A VPN to navigate safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'm not a Rich guy that can afford a Mac, I use a Mac because I feel more comfortable work design, develop Web and iOS applications. I work for years to get a MacBook 2017 before I used to go to my university to use design tools.&lt;/p&gt;

&lt;p&gt;I would appreciate it if you can share the tools you use in your day-to-day.&lt;/p&gt;

&lt;p&gt;Have a nice day!&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>tools</category>
    </item>
    <item>
      <title>How to use ULID as primary key Rails</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Thu, 21 May 2020 19:54:29 +0000</pubDate>
      <link>https://forem.com/renzodiaz/how-to-use-ulid-as-primary-key-rails-5hf6</link>
      <guid>https://forem.com/renzodiaz/how-to-use-ulid-as-primary-key-rails-5hf6</guid>
      <description>&lt;h3&gt;
  
  
  A short story
&lt;/h3&gt;

&lt;p&gt;Ruby on Rails is a powerful framework it is easy to create APIs and fast website development, one thing that came when I was developing an API was how to change the &lt;em&gt;Integer auto-increment primary key&lt;/em&gt; because having a predictable Id can be risky in terms of security if you share it for a client-size application, the user can guess ids and modify the database in the worst of the case, at that moment I had to research some alternatives and I found &lt;a href="https://en.wikipedia.org/wiki/Universally_unique_identifier"&gt;UUID&lt;/a&gt; a non-sequential data type that can be easily be implemented in Rails, so I've tried it and everything was ok until I had to sort &lt;em&gt;ASC&lt;/em&gt; and &lt;em&gt;DESC&lt;/em&gt;, I realize that the methods &lt;code&gt;Model.first&lt;/code&gt; and &lt;code&gt;Model.last&lt;/code&gt; doesn't work as expected because Rails by default uses them with the &lt;em&gt;integer sequential id&lt;/em&gt; type, so as &lt;strong&gt;UUID&lt;/strong&gt; is non-sequential it generates a random Hash like this &lt;code&gt;123e4567-e89b-12d3-a456-426614174000&lt;/code&gt; as it doesn't have a sequence Rails can't figure out how to sort the data, then I researched a little bit more to see if there is any other solution and I found that we can use &lt;code&gt;self.implicit_order_column = "created_at"&lt;/code&gt; in the Model, with this, those methods work ok it sorts depending on the &lt;em&gt;DateTime&lt;/em&gt; created and I was happy. Not for a long time, when another problem came up! When I used &lt;em&gt;seeds&lt;/em&gt; to fill a bulk of fake data into the database, all the data had the same &lt;em&gt;DateTime&lt;/em&gt; and the sorting failed again, it was the same issue as the initial one. I've decided to look for another alternative and I found &lt;a href="https://github.com/ulid/spec"&gt;ULID&lt;/a&gt;, using this I had everything I've needed the sorting works as expected, it isn't predictable and the store in memory is pretty good. I'm still using it and I don't have any issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  How to implement ULID in Rails?
&lt;/h3&gt;

&lt;p&gt;There is already a &lt;a href="https://github.com/rafaelsales/ulid"&gt;gem&lt;/a&gt; for ruby add it to your &lt;code&gt;Gemfile&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#Gemfile&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'ulid'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;bundle install&lt;/code&gt; to install it. Then update your migration or if you are creating a new one should look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# migration&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateUsers&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
   &lt;span class="c1"&gt;# id: false, to not use id int as default&lt;/span&gt;
   &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="c1"&gt;# here we create our id&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:given_name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:family_name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;unique: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:password_digest&lt;/span&gt;
     &lt;span class="o"&gt;...&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;First, we disable the autogenerated id by putting &lt;code&gt;id: false&lt;/code&gt; and create our own id &lt;code&gt;t.binary :id, limit: 16, primary_key: true&lt;/code&gt; what we need to do is fill the id before create, we can do it on each Model but my approach was to create a &lt;em&gt;concern&lt;/em&gt; to just include it in the &lt;code&gt;application_record.rb&lt;/code&gt; file so it will apply for all the models.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/concenrs/ulid_pk.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'ulid'&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;UlidPk&lt;/span&gt;
  &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

  &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;before_create&lt;/span&gt; &lt;span class="ss"&gt;:set_ulid&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;set_ulid&lt;/span&gt;
    &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ULID&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;generate&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then in the &lt;code&gt;application_record.rb&lt;/code&gt; include it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/concenrs/ulid_pk.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ApplicationRecord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Base&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;UlidPk&lt;/span&gt;
  &lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;abstract_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;That's it, now it will autogenerate the id before create.&lt;/p&gt;

&lt;h3&gt;
  
  
  Note
&lt;/h3&gt;

&lt;p&gt;if you need to create an association you should put the type on it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# migration&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateEvents&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
   &lt;span class="c1"&gt;# id: false, to not use id int as default&lt;/span&gt;
   &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:users&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
     &lt;span class="c1"&gt;# here we create our id&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;binary&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;limit: &lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;primary_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:description&lt;/span&gt;
     &lt;span class="o"&gt;...&lt;/span&gt;
     &lt;span class="c1"&gt;# Specify the type: binary&lt;/span&gt;
     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;type: :binary&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;index: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;

     &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
   &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Hope this can help you, I'll try to make it more autogenerated when I get the chance to avoid put &lt;code&gt;type: :binary&lt;/code&gt; or &lt;code&gt;id: false&lt;/code&gt; manually. If you have any question feel free to reach me out. Cheers!&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>postgres</category>
      <category>tutorial</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Architect Rails Project - Part 2</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Sun, 08 Dec 2019 21:15:59 +0000</pubDate>
      <link>https://forem.com/renzodiaz/how-to-architect-rails-project-part-2-3be8</link>
      <guid>https://forem.com/renzodiaz/how-to-architect-rails-project-part-2-3be8</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t2sMaH5V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_auto%252Cw_880/https://miro.medium.com/max/1060/1%252ArW0W8dEaRtJzuXRz3RCszA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t2sMaH5V--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%252Cf_auto%252Cfl_progressive%252Cq_auto%252Cw_880/https://miro.medium.com/max/1060/1%252ArW0W8dEaRtJzuXRz3RCszA.jpeg" alt="Modules"&gt;&lt;/a&gt;&lt;br&gt;
In the previous article, we've done setting up our Surface module&lt;br&gt;
&lt;a href="https://dev.to/renzodiaz/how-to-architect-rails-project-part-1-34bp"&gt;How to Architect Rails Project - Part 1&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In this article, we'll cover the following features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Module Admin&lt;/li&gt;
&lt;li&gt;Layouts&lt;/li&gt;
&lt;li&gt;Front-End Scaffolding&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So, let's kick off...&lt;/p&gt;

&lt;p&gt;For our Admin module, we want to get these routes&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="s1"&gt;'domain.com/admins'&lt;/span&gt; &lt;span class="c1"&gt;# Overview, basic reports, charts&lt;/span&gt;
&lt;span class="s1"&gt;'domain.com/admins/blog'&lt;/span&gt; &lt;span class="c1"&gt;# Articles CRUD&lt;/span&gt;
&lt;span class="s1"&gt;'domain.com/admins/work'&lt;/span&gt; &lt;span class="c1"&gt;# Work CRUD&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's add them to our routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# config/routes.rb&lt;/span&gt;
&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="o"&gt;...&lt;/span&gt;
  &lt;span class="n"&gt;namespace&lt;/span&gt; &lt;span class="ss"&gt;:admins&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:blogs&lt;/span&gt;
    &lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:works&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;Why &lt;strong&gt;admins&lt;/strong&gt; and not &lt;strong&gt;admin&lt;/strong&gt; in singular? Because this module was thought to have multiple user types, users that only can publish articles and won't have permissions to publish works.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Moving on, we need to create the views for our Admin module according to our routes.&lt;/p&gt;

&lt;p&gt;Create these files inside &lt;code&gt;app/views&lt;/code&gt; folder&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/views
  ├── admins/blogs
  │   └── index.html.erb
  │   └── show.html.erb
  ├── admins/works
  │   └── index.html.erb
  │   └── show.html.erb
  └── layouts 
      └── admin.html.erb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's add our markup to &lt;code&gt;admin.html.erb&lt;/code&gt; layout, for now, let's copy the content from &lt;code&gt;application.html.erb&lt;/code&gt;. Our layout would look like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Cool right? We have our Admin layout. &lt;/p&gt;

&lt;p&gt;Hold on your horses... We are still using the assets from our &lt;strong&gt;Surface&lt;/strong&gt; module (application), we want to isolate &lt;strong&gt;Admin/Surface&lt;/strong&gt; and make them to not depend on each other, so, let's isolate our Admin assets out of the Surface module.&lt;/p&gt;

&lt;p&gt;Create these files under &lt;code&gt;app/assets/stylesheets&lt;/code&gt;...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/assets/stylesheets
  ├── admin/base
  │   └── _base.scss
  │   └── _variables.scss
  ├── admin/components
  │   └── _header.scss
  │   └── _sidebar.scss
  └── admin.scss
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;So our &lt;code&gt;admin.scss&lt;/code&gt; would look like this...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Base theming&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/base/variables"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/base/base"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Components&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/components/header"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;@import&lt;/span&gt; &lt;span class="s2"&gt;"admin/components/sidebar"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, add this line to our &lt;code&gt;base_controller.rb&lt;/code&gt; under &lt;code&gt;controller/admins&lt;/code&gt; folder&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;We need also to tell rails that we want to use &lt;code&gt;admin.css|admin.js&lt;/code&gt; in our initializer, so add the following line to &lt;code&gt;config/initializers/assets.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;precompile&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="sx"&gt;%w( admin.js admin.css )&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now let's change our &lt;code&gt;admin.html.erb&lt;/code&gt; layout to use &lt;code&gt;admin.css&lt;/code&gt; and remove the current header stylesheet which comes from the &lt;code&gt;Surface&lt;/code&gt; module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Well done! now if we run &lt;code&gt;rails s&lt;/code&gt; and navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt; we should see a blank page. If we inspect elements in the browser we'll see that our styles are coming from &lt;strong&gt;admin&lt;/strong&gt; and not &lt;strong&gt;application&lt;/strong&gt; anymore.&lt;/p&gt;

&lt;p&gt;Alright, Renzo, this is cool but I don't see any component and styles on the page yet. Besides the javascript still comes from the Surface module.&lt;/p&gt;

&lt;p&gt;Before deep into the components and styles, let's add an Admin module for our javascript files, this is pretty much the same as what we did with styles.&lt;/p&gt;

&lt;p&gt;So, let's create an &lt;code&gt;admin.js&lt;/code&gt; file under &lt;code&gt;app/javascript/packs&lt;/code&gt; folder and add the following lines...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/javascript/packs/admin.js&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rails/ujs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;turbolinks&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@rails/activestorage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Change &lt;code&gt;&amp;lt;%= javascript_pack_tag 'application', 'data-turbolinks-track': 'reload' %&amp;gt;&lt;/code&gt; to &lt;code&gt;&amp;lt;%= javascript_pack_tag 'admin', 'data-turbolinks-track': 'reload' %&amp;gt;&lt;/code&gt; in our &lt;code&gt;admin.html.erb&lt;/code&gt; layout&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let's restart our server and see what we got, we should see the same blank page if we navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt;. If we inspect the page we'll see that now our javascript comes from admin.&lt;/p&gt;

&lt;p&gt;Now we have our js/css isolated from the Surface module, let's add Bulma to our project and start building our components.&lt;/p&gt;

&lt;p&gt;Add &lt;a href="https://github.com/joshuajansen/bulma-rails"&gt;Bulma gem&lt;/a&gt; to our &lt;code&gt;Gemfile&lt;/code&gt; at the bottom&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gem "bulma-rails", "~&amp;gt; 0.8.0"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Then run &lt;code&gt;bundle install&lt;/code&gt; in the terminal.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In this article we are going to use Bulma in both modules &lt;code&gt;Surface/Admin&lt;/code&gt;, just to not make this article too long. Remember that we can also isolate to use Bulma for the Admin and any other framework for our Surface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now let's build our layout markup correctly for the Admin module.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!Doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
...
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"patials/admin/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"columns is-fullheight"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"column is-2 is-sidebar-menu is-hidden-mobile"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/admin/sidebar"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"column is-main-content"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Create a &lt;code&gt;admin&lt;/code&gt; folder under &lt;code&gt;app/views/partials&lt;/code&gt; add these files...&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/views/partials/admin/_header.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;nav&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-brand"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      Logo
    &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt; 
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-burger burger"&lt;/span&gt; &lt;span class="na"&gt;data-target=&lt;/span&gt;&lt;span class="s"&gt;"navMenubd-example"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"navMenubd-example"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-end"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item has-dropdown is-hoverable"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-link"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;figure&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"avatar image is-32x32"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;img&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"is-rounded"&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"https://bulma.io/images/placeholders/32x32.png"&lt;/span&gt; &lt;span class="na"&gt;alt=&lt;/span&gt;&lt;span class="s"&gt;""&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;/figure&amp;gt;&lt;/span&gt;
          username
        &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-dropdown is-right"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"navbar-item"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Logout
          &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/views/partials/admin/_sidebar.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;aside&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    General
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Overview'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_blogs_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-label"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Administration
  &lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"menu-list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
      &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'My Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_works_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'My Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;admins_blogs_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&amp;lt;a&amp;gt;&lt;/span&gt;Users&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/aside&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Add some styles&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/base/_variables.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nv"&gt;$body-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;16px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Responsiveness&lt;/span&gt;
&lt;span class="c1"&gt;// 960, 1152, and 1344 have been chosen because they are divisible by both 12 and 16&lt;/span&gt;
&lt;span class="nv"&gt;$tablet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;769px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 960px container + 40px;&lt;/span&gt;
&lt;span class="nv"&gt;$desktop&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1000px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 1152px container + 40;&lt;/span&gt;
&lt;span class="nv"&gt;$widescreen&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1192px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// 1344px container + 40;&lt;/span&gt;
&lt;span class="nv"&gt;$fullhd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1384px&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nv"&gt;$white&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mh"&gt;#fff&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$orange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;hsl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;17&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;83%&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;57%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$primary&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$orange&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$primary-invert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Link colors&lt;/span&gt;
&lt;span class="nv"&gt;$link&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$primary&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$link-invert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;&lt;span class="mi"&gt;.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/base/_base.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nt"&gt;body&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;overflow-x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;hidden&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.columns&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nc"&gt;.is-fullheight&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nl"&gt;min-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;calc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100vh&lt;/span&gt; &lt;span class="nf"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$navbar-height&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;.75rem&lt;/span&gt; &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;flex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;flex-direction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;justify-content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;stretch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="nc"&gt;.column&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;overflow-y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;    
&lt;span class="nc"&gt;.is-main-content&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$grey-lighter&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt;  &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt; &lt;span class="m"&gt;1rem&lt;/span&gt; &lt;span class="m"&gt;2rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/components/_sidebar.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.is-sidebar-menu&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="mi"&gt;.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$grey-dark&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nt"&gt;li&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nt"&gt;a&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$white&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;code&gt;app/assets/stylesheets/admin/components/_header.scss&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="nc"&gt;.navbar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.navbar-item&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nc"&gt;.avatar&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="nl"&gt;margin-right&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;.65rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="nc"&gt;.is-rounded&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nl"&gt;max-height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Alright, if we restart our browser at this point and navigate to &lt;code&gt;http://localhost:3000/admins/blogs&lt;/code&gt; our dashboard should look like this.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--23YuF5rx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1919/1%2AlP3nTaT-q9Wz1rHfPFcd3g.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--23YuF5rx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1919/1%2AlP3nTaT-q9Wz1rHfPFcd3g.png" alt="Portfolio | Admin"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you got any error through this setup, feel free to drop me a line &lt;a href="mailto:dev.renzo.diaz@gmail.com"&gt;dev.renzo.diaz@gmail.com&lt;/a&gt; I’ll try to answer all your doubts asap.&lt;/p&gt;

&lt;p&gt;In part 1 of this article, I mentioned this part 2 will cover CRUDS and Authentication, and React as well.&lt;/p&gt;

&lt;p&gt;We'll, we will cover those points in part3, I don't want to make this article too long and don't mix with Rails back-end stuff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Architect Rails Project - Part 1</title>
      <dc:creator>Renzo Diaz</dc:creator>
      <pubDate>Wed, 04 Dec 2019 16:31:57 +0000</pubDate>
      <link>https://forem.com/renzodiaz/how-to-architect-rails-project-part-1-34bp</link>
      <guid>https://forem.com/renzodiaz/how-to-architect-rails-project-part-1-34bp</guid>
      <description>&lt;p&gt;When I started to build web applications with ruby on rails, I had a lot of questions that I figured out through the years of using the framework. Now in this article, I’m going to share the base structure to start building a web application.&lt;/p&gt;

&lt;p&gt;Depending on the requirements our structure could slightly change, in this scenario, I’m going to build a simple &lt;strong&gt;portfolio app&lt;/strong&gt; with a blog of articles.&lt;/p&gt;

&lt;p&gt;Before start building our project, let’s imagine two layers one to administrate the site and the other for public access (front pages). Here is how it should look.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1060/1%2ArW0W8dEaRtJzuXRz3RCszA.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vfnMpkSb--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1060/1%2ArW0W8dEaRtJzuXRz3RCszA.jpeg" alt="Modules"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin (Dashboard):&lt;/strong&gt;&lt;br&gt;
Only admin users have access and permissions to post articles, add portfolio jobs, see reports, etc.&lt;br&gt;
There are many ways to build modules, for example using ActiveAdmin gem we can crate the admin module, another way is creating engines, but we are going to set up our dashboard module from scratch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surface (front pages):&lt;/strong&gt;&lt;br&gt;
In this module, we’ll put all the public pages like home, about, portfolio, blog, and contact.&lt;br&gt;
Once we have clear this concept, let’s kick-off creating or project…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rails new portfolio &lt;span class="nt"&gt;-d&lt;/span&gt; postgresql &lt;span class="nt"&gt;-T&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;-T = skip testing modules. We’ll focus on test and design in other article&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Move under the Portfolio directory and run the following commands to create the database and migrations…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;rails db:create
rails db:migrate
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Until here, we have a clean project with no custom pages or modules. So, let’s start creating our project architecture.&lt;br&gt;
First, under controllers create 2 folders called (admins, surface). Our tree would look like this.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/controllers
  ├── admins
  └── surface
  └── application_controller.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Create these controllers under admins and surface folders &lt;code&gt;base_controller.rb&lt;/code&gt;, &lt;code&gt;blogs_controller.rb&lt;/code&gt;, &lt;code&gt;works_controller.rb&lt;/code&gt; and &lt;code&gt;pages_controller.rb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/controllers
  ├── admins # Portfolio blog and work management
  │   └── base_controller.rb
  |   └── blogs_controller.rb
  |   └── works_controller.rb
  └── surface # Public pages
  │   └── pages_controller.rb
  └── application.rb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s add some code to our admin controllers…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# admins/base_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BaseController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;# Define what users can access to this module&lt;/span&gt;
  &lt;span class="c1"&gt;# Define what view layout to use &lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#admins/works_controller.rb
# here we are extending from our Admins::BaseControler
class Admins::WorksController &amp;lt; Admins::BaseController
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;





&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# admins/blogs_controllers&lt;/span&gt;
&lt;span class="c1"&gt;# Inherits from Admins::BaseController&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Admins::BlogsController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Admins&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;BaseController&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Let’s add some code to our surface controller…&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# surface/pages_controller.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Surface::PagesController&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationController&lt;/span&gt;
  &lt;span class="c1"&gt;#define our pages&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;home&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;about&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;work&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;blog&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;contact&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, we are going to add some routes to our app un &lt;code&gt;config/routes.rb&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Rails&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;draw&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# Define our root page&lt;/span&gt;
  &lt;span class="n"&gt;root&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'surface/pages#home'&lt;/span&gt;

  &lt;span class="c1"&gt;# Frontend pages&lt;/span&gt;

  &lt;span class="c1"&gt;# scope module 'surface' to get this format url `domain.com/about, domain.com/work...` &lt;/span&gt;
  &lt;span class="c1"&gt;# and not `domain.com/surface/about...`&lt;/span&gt;
  &lt;span class="n"&gt;scope&lt;/span&gt; &lt;span class="ss"&gt;module: &lt;/span&gt;&lt;span class="s1"&gt;'surface'&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#about'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_about'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_work'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_blog'&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s1"&gt;'/contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s1"&gt;'pages#contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;as: &lt;/span&gt;&lt;span class="s1"&gt;'surface_contact'&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;These routes also can be isolated in files, we’ll isolate these routes in the next article.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Now, we should create our views according to our modules, let’s focus first in our Surface module, so let’s create the following folders and views.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/views
  ├── surface/pages # Write in each file &amp;lt;h1&amp;gt;Page Name&amp;lt;/h1&amp;gt; 
  │   └── home.html.erb     # I.e &amp;lt;h1&amp;gt;Home&amp;lt;/h1&amp;gt;
  │   └── about.html.erb    # I.e &amp;lt;h1&amp;gt;About&amp;lt;/h1&amp;gt;
  |   └── work.html.erb     # I.e &amp;lt;h1&amp;gt;Work&amp;lt;/h1&amp;gt;
  |   └── blog.html.erb     # I.e &amp;lt;h1&amp;gt;Blog&amp;lt;/h1&amp;gt;
  |   └── contact.html.erb  # I.e &amp;lt;h1&amp;gt;Contact&amp;lt;/h1&amp;gt;
  └── partials   
  │   └── _header.html.erb
  |   └── _footer.html.erb
  └── layouts 
  |   └── application.html.erb
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Now let’s edit &lt;code&gt;application.html.erb&lt;/code&gt; under the layout folder and add the header and footer partials.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Partials are the components that will be used across the site.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Let’s add some Html links to our &lt;code&gt;_header.html.erb&lt;/code&gt; under the partials folder...&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Logo&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;nav&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Home'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;root_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'About'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_about_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Work'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_work_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Blog'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_blog_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;link_to&lt;/span&gt; &lt;span class="s1"&gt;'Contact'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;surface_contact_path&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/nav&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Next, add the header partial to our layout &lt;code&gt;applicaton.html.erb&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight erb"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!DOCTYPE html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;head&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Portfolio&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csrf_meta_tags&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;csp_meta_tag&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;stylesheet_link_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;media: &lt;/span&gt;&lt;span class="s1"&gt;'all'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;javascript_pack_tag&lt;/span&gt; &lt;span class="s1"&gt;'application'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'data-turbolinks-track'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'reload'&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/head&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="n"&gt;render&lt;/span&gt; &lt;span class="s2"&gt;"partials/header"&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
    &lt;span class="cp"&gt;&amp;lt;%=&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="cp"&gt;%&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Alright! we have our surface module working perfectly 💯 if you go tho &lt;code&gt;http://localhost:3000&lt;/code&gt; you will be able to navigate between the pages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--NfZt6XAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://miro.medium.com/max/600/1%2AXtKeK23_ESvngy7PnCmX4g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--NfZt6XAk--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_66%2Cw_880/https://miro.medium.com/max/600/1%2AXtKeK23_ESvngy7PnCmX4g.gif" alt="demo"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you got any error through this setup, feel free to drop me a line &lt;a href="mailto:dev.renzo.diaz@gmail.com"&gt;dev.renzo.diaz@gmail.com&lt;/a&gt; I’ll try to answer all your doubts asap.&lt;/p&gt;

&lt;p&gt;In the next &lt;a href="https://dev.to/renzodiaz/how-to-architect-rails-project-part-2-3be8"&gt;Part 2&lt;/a&gt;, will cover the admin module, authentication users, dashboard layout, CRUDS, and some of the front-end stuff.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.buymeacoffee.com/renzodiaz"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--PT2F9GG6--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://cdn.buymeacoffee.com/buttons/bmc-new-btn-logo.svg" alt="Buy me a coffee"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
      <category>rails</category>
      <category>tutorial</category>
      <category>ruby</category>
      <category>architecture</category>
    </item>
  </channel>
</rss>
