Update docs. Implemented a one-time token to work around domain cookies not being immediately return by the browser. Reduce db queries on /api/verify requests.
This commit is contained in:
@@ -10,15 +10,19 @@ module Api
|
||||
def verify
|
||||
# Note: app_slug parameter is no longer used - we match domains directly with ForwardAuthRule
|
||||
|
||||
# Get the session from cookie
|
||||
session_id = extract_session_id
|
||||
# Check for one-time forward auth token first (to handle race condition)
|
||||
session_id = check_forward_auth_token
|
||||
|
||||
# If no token found, try to get session from cookie
|
||||
session_id ||= extract_session_id
|
||||
|
||||
unless session_id
|
||||
# No session cookie - user is not authenticated
|
||||
# No session cookie or token - user is not authenticated
|
||||
return render_unauthorized("No session cookie")
|
||||
end
|
||||
|
||||
# Find the session
|
||||
session = Session.find_by(id: session_id)
|
||||
# Find the session with user association (eager loading for performance)
|
||||
session = Session.includes(:user).find_by(id: session_id)
|
||||
unless session
|
||||
# Invalid session
|
||||
return render_unauthorized("Invalid session")
|
||||
@@ -30,10 +34,10 @@ module Api
|
||||
return render_unauthorized("Session expired")
|
||||
end
|
||||
|
||||
# Update last activity
|
||||
# Update last activity (skip validations for performance)
|
||||
session.update_column(:last_activity_at, Time.current)
|
||||
|
||||
# Get the user
|
||||
# Get the user (already loaded via includes(:user))
|
||||
user = session.user
|
||||
unless user.active?
|
||||
return render_unauthorized("User account is not active")
|
||||
@@ -44,8 +48,12 @@ module Api
|
||||
forwarded_host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
|
||||
|
||||
if forwarded_host.present?
|
||||
# Load active rules with their associations for better performance
|
||||
# Preload groups to avoid N+1 queries in user_allowed? checks
|
||||
rules = ForwardAuthRule.includes(:groups).active
|
||||
|
||||
# Find matching forward auth rule for this domain
|
||||
rule = ForwardAuthRule.active.find { |r| r.matches_domain?(forwarded_host) }
|
||||
rule = rules.find { |r| r.matches_domain?(forwarded_host) }
|
||||
|
||||
unless rule
|
||||
Rails.logger.warn "ForwardAuth: No rule found for domain: #{forwarded_host}"
|
||||
@@ -91,13 +99,30 @@ module Api
|
||||
|
||||
private
|
||||
|
||||
def check_forward_auth_token
|
||||
# Check for one-time token in query parameters (for race condition handling)
|
||||
token = params[:fa_token]
|
||||
return nil unless token.present?
|
||||
|
||||
# Try to get session ID from cache
|
||||
session_id = Rails.cache.read("forward_auth_token:#{token}")
|
||||
return nil unless session_id
|
||||
|
||||
# Verify the session exists and is valid
|
||||
session = Session.find_by(id: session_id)
|
||||
return nil unless session && !session.expired?
|
||||
|
||||
# Delete the token immediately (one-time use)
|
||||
Rails.cache.delete("forward_auth_token:#{token}")
|
||||
|
||||
session_id
|
||||
end
|
||||
|
||||
def extract_session_id
|
||||
# Extract session ID from cookie
|
||||
# Rails uses signed cookies by default
|
||||
session_id = cookies.signed[:session_id]
|
||||
Rails.logger.info "ForwardAuth: Session cookie present: #{session_id.present?}, value: #{session_id&.to_s&.first(10)}..."
|
||||
Rails.logger.info "ForwardAuth: All cookies: #{cookies.to_h.keys.join(', ')}"
|
||||
session_id
|
||||
session_id
|
||||
end
|
||||
|
||||
def extract_app_from_headers
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
require 'uri'
|
||||
|
||||
module Authentication
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
@@ -36,9 +38,7 @@ module Authentication
|
||||
|
||||
def after_authentication_url
|
||||
return_url = session[:return_to_after_authenticating]
|
||||
Rails.logger.info "Authentication: after_authentication_url - session[:return_to_after_authenticating] = #{return_url.inspect}"
|
||||
final_url = session.delete(:return_to_after_authenticating) || root_url
|
||||
Rails.logger.info "Authentication: Final redirect URL: #{final_url}"
|
||||
final_url
|
||||
end
|
||||
|
||||
@@ -60,9 +60,11 @@ module Authentication
|
||||
# Set domain for cross-subdomain authentication if we can extract it
|
||||
cookie_options[:domain] = domain if domain.present?
|
||||
|
||||
Rails.logger.info "Authentication: Setting session cookie with options: #{cookie_options.except(:value).merge(value: cookie_options[:value]&.to_s&.first(10) + '...')}"
|
||||
Rails.logger.info "Authentication: Extracted domain from #{request.host}: #{domain.inspect}"
|
||||
cookies.signed.permanent[:session_id] = cookie_options
|
||||
cookies.signed.permanent[:session_id] = cookie_options
|
||||
|
||||
# Create a one-time token for immediate forward auth after authentication
|
||||
# This solves the race condition where browser hasn't processed cookie yet
|
||||
create_forward_auth_token(session)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -103,4 +105,35 @@ module Authentication
|
||||
root_parts = parts[-2..-1]
|
||||
".#{root_parts.join('.')}"
|
||||
end
|
||||
|
||||
# Create a one-time token for forward auth to handle the race condition
|
||||
# where the browser hasn't processed the session cookie yet
|
||||
def create_forward_auth_token(session_obj)
|
||||
# Generate a secure random token
|
||||
token = SecureRandom.urlsafe_base64(32)
|
||||
|
||||
# Store it with an expiry of 30 seconds
|
||||
Rails.cache.write(
|
||||
"forward_auth_token:#{token}",
|
||||
session_obj.id,
|
||||
expires_in: 30.seconds
|
||||
)
|
||||
|
||||
# Set the token as a query parameter on the redirect URL
|
||||
# We need to store this in the controller's session
|
||||
controller_session = session
|
||||
if controller_session[:return_to_after_authenticating].present?
|
||||
original_url = controller_session[:return_to_after_authenticating]
|
||||
uri = URI.parse(original_url)
|
||||
|
||||
# Add token as query parameter
|
||||
query_params = URI.decode_www_form(uri.query || "").to_h
|
||||
query_params['fa_token'] = token
|
||||
uri.query = URI.encode_www_form(query_params)
|
||||
|
||||
# Update the session with the tokenized URL
|
||||
controller_session[:return_to_after_authenticating] = uri.to_s
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -67,6 +67,12 @@ class SessionsController < ApplicationController
|
||||
if request.post?
|
||||
code = params[:code]&.strip
|
||||
|
||||
# Check if user is already authenticated (prevent duplicate submissions)
|
||||
if authenticated?
|
||||
redirect_to root_path, notice: "Already signed in."
|
||||
return
|
||||
end
|
||||
|
||||
# Try TOTP verification first
|
||||
if user.verify_totp(code)
|
||||
session.delete(:pending_totp_user_id)
|
||||
|
||||
Reference in New Issue
Block a user