Rails 8 authentication, via a React frontend.

Having gratefully used Devise as my Rails app authentication library for years, I’ve been keen to try out Rails 8’s new built in authentication generator for a while. I now find myself working on a brand new app – React + Rails API – which needs authen…


This content originally appeared on DEV Community and was authored by jbk

Having gratefully used Devise as my Rails app authentication library for years, I've been keen to try out Rails 8's new built in authentication generator for a while. I now find myself working on a brand new app – React + Rails API – which needs authentication & session logic, and so a good opportunity to try Rails auth. I put this post together to document the steps that I took, for my simple implementation, in case it helps others.
(Rails 8.0.1 built as an API app, React 19.1)
(Password reset functionality not discussed in detail here, it will be in another post)

📜 Rails 8 Auth - General Paradigms

(Rails auth guide here)

Authentication Concern

Most of the Rails 8 auth logic resides in the Authentication module concern. This module handles:

  • creating and loading the session.
  • authenticating the user.
  • setting the Current context.

It runs require_authentication as a before_action for all controller actions. Except an action from authentication by passing the action to an allow_unauthenticated_access call. It provides the helper method authenticated?, available in views for easy conditional rendering.

DB-Backed Sessions

Sensitive data (e.g. user_id) is stored in a Session model on the server. The client only stores a session_id cookie holding the id of the Session db record (this cookie is set in the Authentication module's #start_new_session_for(user) method).

  • More secure – user data stays server-side.
  • Scalable – supports multiple devices and multiple sessions. Stores useful info like user agent & IP address in the Session db record.

Request context via Current

Rails 8 uses ActiveSupport::CurrentAttributes to provide a thread-safe, per-request context. Use:

  • Current.session – to get the current DB-backed session object.
  • Current.user – to get the current authenticated user. (e.g.'s of Current object being set can be seen in the Authentication module's; #start_new_session_for(user), resume_session & terminate_session methods)

With Current available anywhere, except views, this replaces the older current_user patterns (authenticated? helper is available in views).

🔧 Implementation

Rails logo

Rails - Back End

1) Generate

Run:

bin/rails generate authentication
bin/rails db:migrate

Read your logs output to see what was generated for you, but in summary:

  • Users model - with has_secure_password - this encrypts password into a password_digest column (hashed via bcrypt) & provides ActiveSupport's MessageVerifier & TokenFor modules, required for the password reset functionality.
  • Session model & sessions table - belongs_to :user, User has_many :sessions.
  • Sessions & Passwords controllers.
  • Authentication concern.
  • Current context module.
  • Passwords new and edit view and reset mailer & html.

The generator provides all logic, controllers, actions, migrations for user authentication, and the mailers and logic for password reset. However it does not provide views for user or session creation, editing or deleting – we must write these ourselves.

A read of the generated Authentication module concern will greatly help your understanding of how the User, Session & Current models work together to produce the authentication logic.

2) Configure

You must configure your Rails app to allow requests from a different origin (i.e. React). Add gem "rack-cors" to gemfile, run bundle install then add a cors.rb initializer:

config/initializers/cors.rb:

Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins Rails.credentials.trusted_origins
    resource "*",
      headers: :any,
      methods: %i[get post put patch delete options head],
      credentials: true
  end
end

Note:

  • set your trusted_origins credential for dev & prod (bin/rails credentials:edit --environment development).
  • credentials: true enables cookies to be passed with each request.

Read Authentication concern's #start_new_session_for(user) cookie creation, note:

  • httponly: true ensures JS cannot access the cookie.
  • same_site: :lax ensures that a different origin can make requests from the server.
  • secure: true ensures cookies are only sent over https in production.

Set Rails' CSRF settings to allow requests from React app's server Origin:
Add this to app/controllers/application_controller.rb

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception

  private
  def verified_request?
    origins = Rails.application.credentials.trusted_origins || []
    valid = super || origins.include?(request.origin)
    Rails.logger.warn("Blocked CSRF request from #{request.origin}") unless valid
    valid
  end
end

This ensures that an exception is thrown if a valid authenticity_token is not found & allows requests from those non-same origins explicitly set in credentials.trusted_origins.

3) Control

Write API namespaced Users controller with create action:

class Api::UsersController < ApplicationController
  allow_unauthenticated_access only: %i[ create ]

  def create
    @user = User.new(user_params)
    if @user.save
      render json: { user: @user, notice: "User created successfully" }
    else
      render json: { errors: @user.errors.full_messages, status: :unprocessable_entity }
    end
  end

  private
  def user_params
    params.require(:user).permit(:email_address, :password, :password_confirmation)
  end
end

Write API namespaced Sessions controller with create, show and destroy actions:

class Api::SessionsController < ApplicationController
  allow_unauthenticated_access only: %i[ create ]

  def create
    if user = User.authenticate_by(params.permit(:email_address, :password))
      start_new_session_for user
      render json: { user: user, authenticated: true, notice: "Successfully logged in" }
    else
      render json: { error: "Invalid email or password" }, status: :unauthorized
    end
  end

  def show
    if Current.session.user
      render json: { authenticated: true, user: Current.session.user }
    else
      render json: { authenticated: false, error: "Not logged in" }, status: :unauthorized
    end
  end

  def destroy
    begin
      terminate_session
      render json: { notice: 'successfully logged out' }
    rescue => e
      render json: { error: "Failed to log out: #{e.message}" }, status: :internal_server_error
    end
  end

end

Points to note:

  • Sessions#show - for authentication of existing session route.
  • The #create action on each needs unauthenticated access.

4) Route
Create the routes:
config/routes.rb

 namespace :api do
    resource :session, only: [:create, :show, :destroy]
    resource :user, only: [:create]
  end

React Logo

React - Front End

1) Configure
In order for relative paths in your fetch requests to route to the correct host, i.e. your rails server, add server proxy to either vite.config.js – if you built with vite, or to your package.json – if you built with Create React App:
vite.config.js:

export default {
  server: {
    proxy: {
      '/api': 'http://localhost:3000'
    }
  }
}

package.json:

{
  ...
  "proxy": "http://localhost:3000"
  ...
}

2) Fetch
Build your various fetches:
sign up (user#create)

...
fetch("/api/user", {
        method: "POST",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          user: {
            email_address: e.target.email_address.value,
            password: e.target.password.value,
            password_confirmation: e.target.password_confirmation.value,
          },
        }),
      })
      .then(async (res) => {
        const data = await res.json();
        if (res.ok) {
          setIsSignUp(false);
          showAlert(data.notice);
        } else {
          showAlert(
            data.errors
              ? data.errors.join(", ")
              : "Sign up failed, fetch response was not ok, and no JSON response errors object"
          );
        }
      })
      .catch((err) => {
        showAlert(err.message || "Sign up failed, fetch threw an error, and there was no err.message object");
      });
...

sign in (session#create)

...
      fetch("/api/session", {
        method: "POST",
        credentials: "include",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          email_address: e.target.email_address.value,
          password: e.target.password.value,
        }),
      })
      .then(async (res) {
        const data = await res.json();
        if(res.ok) {
          handleSignIn(data);
          showAlert(data.notice);
        } else {
          showAlert(data.error || "Sign in failed, fetch response not ok, and was no JSON response errors object");
        }
      })
      .catch((err) => {
        showAlert(err.message || "Sign in failed, fetch threw an error, and there was no err.message object");
      });
...

authenticate current session, i.e. the session_id cookie, if present

...
 fetch('/api/session', { credentials: 'include' })
    .then(async res => {
      const data = await res.json();
      if(res.ok) {
        setAuthChecked(true);
        setUser(data.user);
        setLoggedIn(data.authenticated);  
      } else {
        setLoggedIn(data.authenticated);  
        setAuthChecked(true);
        showAlert(data.error || "Authentication failed, fetch response not ok, and was no JSON response errors object");  
      }
    })
    .catch(() => setAuthChecked(true));
...

sign out (session#destroy)

...
fetch('/api/session', {
      method: 'DELETE'
    })
    .then(async res => {
      const data = await res.json();
      if (res.ok) {
        showAlert(data.notice)
        setUser(null)
        setLoggedIn(false)
      } else {
        showAlert(data.errors
          ? data.errors.join(', ')
          : "Log out failed, and no errors object in JSON response"
        )
      }
    })
...

points of note:

  • for brevity React state code omitted.
  • sign up, sign in and sign out fetches triggered from event callbacks (i.e. from sign up/in form submission, or log out button click).
  • the authentication fetch is called on each page reload/full App remount.
  • local loggedIn state must be set, done by the sign_in or authenticate current session fetches, for the user to be served anything other than the sign in or sign up form.
  • Note that credentials: 'include' in the fetch options ensures the browser will send any related cookies with the request and set any cookies received in the response.

Production considerations:

  • Serve both Rails and React over HTTPS.
  • Set your 'trusted_origins' values for dev & prod.
  • If using subdomains, set the cookie domain accordingly.

Hopefully this may help a few readers get similar up and running. An evolved implementation for me will be abstracting the React authentication fetches out of components. Building a Registrations controller in the Rails app. Implementing sign up mailers and password reset on the front end. I intend to update this post with those evolutions once completed, and to create another post where i discuss the Rails auth password_reset mechanism in more detail. Thanks for reading, any code suggestions/improvements/corrrections please do let me know.


This content originally appeared on DEV Community and was authored by jbk


Print Share Comment Cite Upload Translate Updates
APA

jbk | Sciencx (2025-06-07T13:35:57+00:00) Rails 8 authentication, via a React frontend.. Retrieved from https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/

MLA
" » Rails 8 authentication, via a React frontend.." jbk | Sciencx - Saturday June 7, 2025, https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/
HARVARD
jbk | Sciencx Saturday June 7, 2025 » Rails 8 authentication, via a React frontend.., viewed ,<https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/>
VANCOUVER
jbk | Sciencx - » Rails 8 authentication, via a React frontend.. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/
CHICAGO
" » Rails 8 authentication, via a React frontend.." jbk | Sciencx - Accessed . https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/
IEEE
" » Rails 8 authentication, via a React frontend.." jbk | Sciencx [Online]. Available: https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/. [Accessed: ]
rf:citation
» Rails 8 authentication, via a React frontend. | jbk | Sciencx | https://www.scien.cx/2025/06/07/rails-8-authentication-via-a-react-frontend/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.