Use Rails' new sessions generator to replace the existing setup
Ref: https://github.com/rails/rails/pull/52328
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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,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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user