--- name: inertia-rails-auth description: Implement authentication and authorization in Inertia Rails applications. Use when setting up login, sessions, permissions, and access control with Devise, has_secure_password, or other auth solutions. license: MIT metadata: author: community version: "1.0.0" user-invocable: true --- # Inertia Rails Authentication & Authorization Guide to implementing authentication and authorization in Inertia Rails applications. ## Key Principle Inertia uses your existing Rails authentication infrastructure. No special OAuth or token-based auth required. Since your frontend and backend share the same domain, session-based auth works seamlessly. ## Authentication with Devise ### Setup ```ruby # Gemfile gem 'devise' ``` ```bash bundle install rails generate devise:install rails generate devise User rails db:migrate ``` ### Share Authentication State ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base inertia_share do { auth: { user: current_user&.as_json(only: [:id, :name, :email, :avatar_url]), signed_in: user_signed_in? } } end end ``` ### Create Login Page ```ruby # app/controllers/sessions_controller.rb class SessionsController < Devise::SessionsController def new render inertia: {} end def create self.resource = warden.authenticate(auth_options) if resource sign_in(resource_name, resource) redirect_to after_sign_in_path_for(resource), notice: 'Signed in successfully!' else redirect_to new_session_path(resource_name), inertia: { errors: { email: 'Invalid email or password' } } end end def destroy sign_out(resource_name) redirect_to root_path, notice: 'Signed out successfully!' end end ``` ### Login Component (Vue) ```vue ``` ### Login Component (React) ```jsx // app/frontend/pages/sessions/new.jsx import { useForm, Link } from '@inertiajs/react' export default function Login() { const { data, setData, post, processing, errors, reset } = useForm({ email: '', password: '', remember: false, }) function submit(e) { e.preventDefault() post('/users/sign_in', { onSuccess: () => reset('password'), }) } return (
) } ``` ## Authentication with has_secure_password ### User Model ```ruby # app/models/user.rb class User < ApplicationRecord has_secure_password validates :email, presence: true, uniqueness: true validates :password, length: { minimum: 8 }, allow_nil: true end ``` ### Sessions Controller ```ruby # app/controllers/sessions_controller.rb class SessionsController < ApplicationController skip_before_action :authenticate!, only: [:new, :create] def new render inertia: {} end def create user = User.find_by(email: params[:email]) if user&.authenticate(params[:password]) session[:user_id] = user.id redirect_to dashboard_path, notice: 'Welcome back!' else redirect_to login_path, inertia: { errors: { email: 'Invalid email or password' } } end end def destroy session.delete(:user_id) redirect_to root_path, notice: 'Signed out successfully!' end end # Routes # config/routes.rb get 'login', to: 'sessions#new' post 'login', to: 'sessions#create' delete 'logout', to: 'sessions#destroy' ``` ### Application Controller Helper ```ruby # app/controllers/application_controller.rb class ApplicationController < ActionController::Base before_action :authenticate! helper_method :current_user, :user_signed_in? inertia_share do { auth: { user: current_user&.as_json(only: [:id, :name, :email]), signed_in: user_signed_in? } } end private def current_user @current_user ||= User.find_by(id: session[:user_id]) end def user_signed_in? current_user.present? end def authenticate! unless user_signed_in? redirect_to login_path, alert: 'Please sign in to continue' end end end ``` ## Authorization ### Passing Permissions as Props Since frontend components can't access server-side authorization helpers, pass permission results as props: ```ruby # app/controllers/users_controller.rb class UsersController < ApplicationController def index render inertia: { can: { create_user: policy(User).create? }, users: User.all.map do |user| serialize_user(user) end } end def show user = User.find(params[:id]) render inertia: { user: serialize_user(user), can: { edit: policy(user).edit?, delete: policy(user).destroy? } } end private def serialize_user(user) user.as_json(only: [:id, :name, :email]).merge( can: { edit: policy(user).edit?, delete: policy(user).destroy? } ) end end ``` ### Using Pundit ```ruby # Gemfile gem 'pundit' # app/controllers/application_controller.rb class ApplicationController < ActionController::Base include Pundit::Authorization rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized private def user_not_authorized redirect_to root_path, alert: 'You are not authorized to perform this action' end end # app/policies/user_policy.rb class UserPolicy < ApplicationPolicy def index? true end def show? true end def create? user.admin? end def update? user.admin? || record == user end def destroy? user.admin? && record != user end end ``` ### Using Action Policy ```ruby # Gemfile gem 'action_policy' # app/controllers/users_controller.rb class UsersController < ApplicationController def index render inertia: { can: { create_user: allowed_to?(:create?, User) }, users: User.all.map do |user| user.as_json(only: [:id, :name]).merge( can: { edit: allowed_to?(:edit?, user), delete: allowed_to?(:destroy?, user) } ) end } end end ``` ### Frontend Permission Checks (Vue) ```vue