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 - 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
, Userhas_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 - 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

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/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.