Pass the redirect url through the forms
Some checks failed
CI / scan_ruby (push) Has been cancelled
CI / scan_js (push) Has been cancelled
CI / lint (push) Has been cancelled
CI / test (push) Has been cancelled
CI / system-test (push) Has been cancelled

This commit is contained in:
Dan Milne
2025-10-24 11:36:11 +11:00
parent 9be6ef09ff
commit ad70841689
4 changed files with 42 additions and 33 deletions

View File

@@ -1,15 +1,15 @@
module Api module Api
class ForwardAuthController < ApplicationController class ForwardAuthController < ApplicationController
# ForwardAuth endpoints don't use sessions or CSRF # ForwardAuth endpoints need session storage for return URL
allow_unauthenticated_access allow_unauthenticated_access
skip_before_action :verify_authenticity_token skip_before_action :verify_authenticity_token
skip_before_action :verify_request_origin
# GET /api/verify # GET /api/verify
# This endpoint is called by reverse proxies (Traefik, Caddy, nginx) # This endpoint is called by reverse proxies (Traefik, Caddy, nginx)
# to verify if a user is authenticated and authorized to access an application # to verify if a user is authenticated and authorized to access a domain
def verify def verify
# Get the application slug from query params or X-Forwarded-Host header # Note: app_slug parameter is no longer used - we match domains directly with ForwardAuthRule
app_slug = params[:app] || extract_app_from_headers
# Get the session from cookie # Get the session from cookie
session_id = extract_session_id session_id = extract_session_id
@@ -40,24 +40,28 @@ module Api
return render_unauthorized("User account is not active") return render_unauthorized("User account is not active")
end end
# If an application is specified, check authorization # Check for forward auth rule authorization
if app_slug.present? # Get the forwarded host for domain matching
application = Application.find_by(slug: app_slug, app_type: "trusted_header", active: true) forwarded_host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
unless application if forwarded_host.present?
Rails.logger.warn "ForwardAuth: Application not found or not configured for trusted_header: #{app_slug}" # Find matching forward auth rule for this domain
return render_forbidden("Application not found or not configured") rule = ForwardAuthRule.active.find { |r| r.matches_domain?(forwarded_host) }
unless rule
Rails.logger.warn "ForwardAuth: No rule found for domain: #{forwarded_host}"
return render_forbidden("No authentication rule configured for this domain")
end end
# Check if user is allowed to access this application # Check if user is allowed by this rule
unless application.user_allowed?(user) unless rule.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{app_slug}" Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{forwarded_host} by rule #{rule.domain_pattern}"
return render_forbidden("You do not have permission to access this application") return render_forbidden("You do not have permission to access this domain")
end end
Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{app_slug}" Rails.logger.info "ForwardAuth: User #{user.email_address} granted access to #{forwarded_host} by rule #{rule.domain_pattern} (policy: #{rule.policy_for_user(user)})"
else else
Rails.logger.info "ForwardAuth: User #{user.email_address} authenticated (no app specified)" Rails.logger.info "ForwardAuth: User #{user.email_address} authenticated (no domain specified)"
end end
# User is authenticated and authorized # User is authenticated and authorized
@@ -87,22 +91,8 @@ module Api
end end
def extract_app_from_headers def extract_app_from_headers
# Try to extract application slug from forwarded headers # This method is deprecated since we now use ForwardAuthRule domain matching
# This is useful when the proxy doesn't pass ?app= param # Keeping it for backward compatibility but it's no longer used
# X-Forwarded-Host might contain the hostname
host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
# Try to match hostname to application
# Format: app-slug.domain.com -> app-slug
if host.present?
# Extract subdomain as potential app slug
parts = host.split(".")
if parts.length >= 2
return parts.first if parts.first != "www"
end
end
nil nil
end end

View File

@@ -16,6 +16,11 @@ class SessionsController < ApplicationController
return return
end end
# Store the redirect URL from forward auth if present
if params[:rd].present?
session[:return_to_after_authenticating] = params[:rd]
end
# Check if user is active # Check if user is active
unless user.active? unless user.active?
redirect_to signin_path, alert: "Your account is not active. Please contact an administrator." redirect_to signin_path, alert: "Your account is not active. Please contact an administrator."
@@ -26,7 +31,11 @@ class SessionsController < ApplicationController
if user.totp_enabled? if user.totp_enabled?
# Store user ID in session temporarily for TOTP verification # Store user ID in session temporarily for TOTP verification
session[:pending_totp_user_id] = user.id session[:pending_totp_user_id] = user.id
redirect_to totp_verification_path # Preserve the redirect URL through TOTP verification
if params[:rd].present?
session[:totp_redirect_url] = params[:rd]
end
redirect_to totp_verification_path(rd: params[:rd])
return return
end end
@@ -57,6 +66,10 @@ class SessionsController < ApplicationController
# Try TOTP verification first # Try TOTP verification first
if user.verify_totp(code) if user.verify_totp(code)
session.delete(:pending_totp_user_id) session.delete(:pending_totp_user_id)
# Restore redirect URL if it was preserved
if session[:totp_redirect_url].present?
session[:return_to_after_authenticating] = session.delete(:totp_redirect_url)
end
start_new_session_for user start_new_session_for user
redirect_to after_authentication_url, notice: "Signed in successfully." redirect_to after_authentication_url, notice: "Signed in successfully."
return return
@@ -65,6 +78,10 @@ class SessionsController < ApplicationController
# Try backup code verification # Try backup code verification
if user.verify_backup_code(code) if user.verify_backup_code(code)
session.delete(:pending_totp_user_id) session.delete(:pending_totp_user_id)
# Restore redirect URL if it was preserved
if session[:totp_redirect_url].present?
session[:return_to_after_authenticating] = session.delete(:totp_redirect_url)
end
start_new_session_for user start_new_session_for user
redirect_to after_authentication_url, notice: "Signed in successfully using backup code." redirect_to after_authentication_url, notice: "Signed in successfully using backup code."
return return

View File

@@ -4,6 +4,7 @@
</div> </div>
<%= form_with url: signin_path, class: "contents" do |form| %> <%= form_with url: signin_path, class: "contents" do |form| %>
<%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %>
<div class="my-5"> <div class="my-5">
<%= form.label :email_address, "Email Address", class: "block font-medium text-sm text-gray-700" %> <%= form.label :email_address, "Email Address", class: "block font-medium text-sm text-gray-700" %>
<%= form.email_field :email_address, <%= form.email_field :email_address,

View File

@@ -8,6 +8,7 @@
</div> </div>
<%= form_with url: totp_verification_path, method: :post, class: "space-y-6" do |form| %> <%= form_with url: totp_verification_path, method: :post, class: "space-y-6" do |form| %>
<%= hidden_field_tag :rd, params[:rd] if params[:rd].present? %>
<div> <div>
<%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %> <%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag :code, <%= text_field_tag :code,