Use Rails' new sessions generator to replace the existing setup

Ref: https://github.com/rails/rails/pull/52328
This commit is contained in:
Jeffrey Hardy
2024-08-20 14:04:41 -04:00
parent 43329387a7
commit e3983d5bbe
11 changed files with 68 additions and 115 deletions
+8 -4
View File
@@ -1,7 +1,5 @@
module ApplicationCable
class Connection < ActionCable::Connection::Base
include Authentication::SessionLookup
identified_by :current_user
def connect
@@ -10,11 +8,17 @@ module ApplicationCable
private
def find_verified_user
if verified_session = find_session_by_cookie
verified_session.user
if session = find_session_by_cookie
session.user
else
reject_unauthorized_connection
end
end
def find_session_by_cookie
if token = cookies.signed[:session_token]
Session.find_signed(token)
end
end
end
end
+30 -46
View File
@@ -1,80 +1,64 @@
module Authentication
extend ActiveSupport::Concern
include SessionLookup
included do
before_action :require_authentication
helper_method :signed_in?
protect_from_forgery with: :exception, unless: -> { authenticated_by.bot_key? }
helper_method :authenticated?
end
class_methods do
def require_unauthenticated_access(**options)
allow_unauthenticated_access **options
before_action :redirect_signed_in_user_to_root, **options
end
def allow_unauthenticated_access(**options)
skip_before_action :require_authentication, **options
before_action :restore_authentication, **options
before_action :resume_session, **options
end
end
private
def signed_in?
Current.user.present?
def authenticated?
Current.session.present?
end
def require_authentication
restore_authentication || request_authentication
resume_session || request_authentication
end
def restore_authentication
def resume_session
if session = find_session_by_cookie
resume_session session
set_current_session session
end
end
def find_session_by_cookie
if token = cookies.signed[:session_token]
Session.find_signed(token)
end
end
def request_authentication
session[:return_to_after_authenticating] = request.url
redirect_to new_session_url
end
def redirect_signed_in_user_to_root
redirect_to root_url if signed_in?
end
def start_new_session_for(user)
user.sessions.start!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
authenticated_as session
end
end
def resume_session(session)
session.resume user_agent: request.user_agent, ip_address: request.remote_ip
authenticated_as session
end
def authenticated_as(session)
Current.user = session.user
set_authenticated_by(:session)
cookies.signed.permanent[:session_token] = { value: session.token, httponly: true, same_site: :lax }
end
def post_authenticating_url
def after_authentication_url
session.delete(:return_to_after_authenticating) || root_url
end
def reset_authentication
def start_new_session_for(user)
user.sessions.create!(user_agent: request.user_agent, ip_address: request.remote_ip).tap do |session|
set_current_session session
end
end
def set_current_session(session)
Current.session = session
cookies.signed.permanent[:session_token] = { value: session.signed_id, httponly: true, same_site: :lax }
end
def terminate_session
Current.session.destroy
cookies.delete(:session_token)
end
def set_authenticated_by(method)
@authenticated_by = method.to_s.inquiry
end
def authenticated_by
@authenticated_by ||= "".inquiry
end
end
+6 -17
View File
@@ -1,32 +1,21 @@
class SessionsController < ApplicationController
allow_unauthenticated_access only: %i[ new create ]
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { render_rejection :too_many_requests }
rate_limit to: 10, within: 3.minutes, only: :create, with: -> { redirect_to new_session_url, alert: "Try again later." }
def new
end
def create
if user = User.active.authenticate_by(user_params)
if user = User.authenticate_by(params.permit(:email_address, :password))
start_new_session_for user
redirect_to post_authenticating_url
redirect_to after_authentication_url
else
render_rejection :unauthorized
redirect_to new_session_url, alert: "Try another email address or password."
end
end
def destroy
reset_authentication
redirect_to root_url
terminate_session
redirect_to new_session_url
end
private
def user_params
params.require(:user).permit(:email_address, :password)
end
def render_rejection(status)
flash[:alert] = "Too many requests or unauthorized."
render :new, status: status
end
end
+2 -5
View File
@@ -1,7 +1,4 @@
class Current < ActiveSupport::CurrentAttributes
attribute :user
def account
user.account
end
attribute :session
delegate :user, to: :session, allow_nil: true
end
-16
View File
@@ -1,19 +1,3 @@
class Session < ApplicationRecord
ACTIVITY_REFRESH_RATE = 1.hour
has_secure_token
belongs_to :user
before_create { self.last_active_at ||= Time.now }
def self.start!(user_agent:, ip_address:)
create! user_agent: user_agent, ip_address: ip_address
end
def resume(user_agent:, ip_address:)
if last_active_at.before?(ACTIVITY_REFRESH_RATE.ago)
update! user_agent: user_agent, ip_address: ip_address, last_active_at: Time.now
end
end
end
+4 -7
View File
@@ -1,15 +1,12 @@
class User < ApplicationRecord
belongs_to :account
has_many :sessions, dependent: :destroy
has_many :splats, dependent: :destroy
has_many :sessions, dependent: :destroy
has_secure_password validations: false
scope :active, -> { where(active: true) }
has_many :splats, dependent: :destroy
def current?
self == Current.user
end
normalizes :email_address, with: ->(value) { value.strip.downcase }
def initials
name.scan(/\b\w/).join
@@ -24,6 +21,6 @@ class User < ApplicationRecord
private
def deactived_email_address
email_address&.gsub(/@/, "-deactivated-#{SecureRandom.uuid}@")
email_address.sub(/@/, "-deactivated-#{SecureRandom.uuid}@")
end
end
+1 -2
View File
@@ -1,5 +1,4 @@
<% content_for(:title) { "Sign in" } %>
<% turbo_page_requires_reload %>
<div class="panel shadow center margin-block-double <%= "shake" if flash[:alert] %>" style="--panel-size: 40ch;">
<%#= image_tag "blob.svg", class: "product__logo center colorize--black", size: 130 %>
@@ -12,7 +11,7 @@
<h1 class="margin-none-block-start margin-block-end-double">Fizzy</h1>
<%= form_with model: User.new, url: session_path, class: "flex flex-column gap" do |form| %>
<%= form_with url: session_path, class: "flex flex-column gap" do |form| %>
<div class="flex align-center gap">
<%= translation_button(:email_address) %>
<label class="flex align-center gap input input--actor txt-large">
@@ -0,0 +1,8 @@
class RemoveUnnecessaryColumnsFromSessions < ActiveRecord::Migration[8.0]
def change
change_table :sessions do |t|
t.remove :last_active_at
t.remove :token
end
end
end
Generated
+1 -4
View File
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2024_08_19_192804) do
ActiveRecord::Schema[8.0].define(version: 2024_08_20_170703) do
create_table "accounts", force: :cascade do |t|
t.string "name", null: false
t.datetime "created_at", null: false
@@ -52,13 +52,10 @@ ActiveRecord::Schema[8.0].define(version: 2024_08_19_192804) do
create_table "sessions", force: :cascade do |t|
t.integer "user_id", null: false
t.string "token", null: false
t.string "ip_address"
t.string "user_agent"
t.datetime "last_active_at", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["token"], name: "index_sessions_on_token", unique: true
t.index ["user_id"], name: "index_sessions_on_user_id"
end
+7 -13
View File
@@ -9,36 +9,30 @@ class SessionsControllerTest < ActionDispatch::IntegrationTest
assert_response :success
end
test "new denied with incompatible browser" do
test "new enforces browser compatibility" do
get new_session_url, env: { "HTTP_USER_AGENT" => DISALLOWED_BROWSER }
assert_select "svg", message: /Your browser is not supported/
end
test "new allowed with compatible browser" do
get new_session_url, env: { "HTTP_USER_AGENT" => ALLOWED_BROWSER }
assert_select "svg", text: /Your browser is not supported/, count: 0
end
test "create with valid credentials" do
post session_url, params: { user: { email_address: "david@37signals.com", password: "secret123456" } }
post session_url, params: { email_address: "david@37signals.com", password: "secret123456" }
assert_redirected_to root_url
assert parsed_cookies.signed[:session_token]
assert cookies[:session_token].present?
end
test "create with invalid credentials" do
post session_url, params: { user: { email_address: "david@37signals.com", password: "wrong" } }
assert_response :unauthorized
assert_nil parsed_cookies.signed[:session_token]
post session_url, params: { email_address: "david@37signals.com", password: "wrong" }
assert_redirected_to new_session_url
assert_not cookies[:session_token].present?
end
test "destroy" do
sign_in :kevin
delete session_url
assert_redirected_to root_url
assert_redirected_to new_session_url
assert_not cookies[:session_token].present?
end
end
+1 -1
View File
@@ -5,7 +5,7 @@ module SessionTestHelper
def sign_in(user)
user = users(user) unless user.is_a? User
post session_url, params: { user: { email_address: user.email_address, password: "secret123456" } }
post session_url, params: { email_address: user.email_address, password: "secret123456" }
assert cookies[:session_token].present?
end