4 Commits

Author SHA1 Message Date
Dan Milne
831bd083c2 Update readme
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
2025-10-24 12:02:38 +11:00
Dan Milne
1212e0f22e Allow redirection to 3rd party sites
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
2025-10-24 11:52:58 +11:00
Dan Milne
a21b21ace2 remove unneeded action
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
2025-10-24 11:43:34 +11:00
Dan Milne
ad70841689 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
2025-10-24 11:36:11 +11:00
5 changed files with 53 additions and 38 deletions

View File

@@ -8,7 +8,13 @@ Clinch gives you one place to manage users and lets any web app authenticate aga
Do you host your own web apps? MeTube, Kavita, Audiobookshelf, Gitea? Rather than managing all those separate user accounts, set everyone up on Clinch and let it do the authentication and user management.
**Clinch is a lightweight alternative to [Authelia](https://www.authelia.com) and [Authentik](https://goauthentik.io)**, designed for simplicity and ease of deployment.
Clinch sits in a sweet spot between two excellent open-source identity solutions:
**[Authelia](https://www.authelia.com)** is a fantastic choice for those who prefer external user management through LDAP and enjoy comprehensive YAML-based configuration. It's lightweight, secure, and works beautifully with reverse proxies.
**[Authentik](https://goauthentik.io)** is an enterprise-grade powerhouse offering extensive protocol support (OAuth2, SAML, LDAP, RADIUS), advanced policy engines, and distributed "outpost" architecture for complex deployments.
**Clinch** offers a middle ground with built-in user management, a modern web interface, and focused SSO capabilities (OIDC + ForwardAuth). It's perfect for users who want self-hosted simplicity without external dependencies or enterprise complexity.
---
@@ -45,7 +51,8 @@ Works with reverse proxies (Caddy, Traefik, Nginx):
3. **401/403** → Proxy redirects to Clinch login; after login, user returns to original URL
Apps that speak OIDC use the OIDC flow; apps that only need "who is it?" headers use ForwardAuth.
Forward Auth works only on the same domain as Clinch runs
**Note:** ForwardAuth requires applications to run on the same domain as Clinch (e.g., `app.yourdomain.com` with Clinch at `auth.yourdomain.com`) for secure session cookie sharing. Take a look at Authentik if you need multi domain support.
### SMTP Integration
Send emails for:

View File

@@ -1,15 +1,14 @@
module Api
class ForwardAuthController < ApplicationController
# ForwardAuth endpoints don't use sessions or CSRF
# ForwardAuth endpoints need session storage for return URL
allow_unauthenticated_access
skip_before_action :verify_authenticity_token
# GET /api/verify
# 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
# Get the application slug from query params or X-Forwarded-Host header
app_slug = params[:app] || extract_app_from_headers
# Note: app_slug parameter is no longer used - we match domains directly with ForwardAuthRule
# Get the session from cookie
session_id = extract_session_id
@@ -40,24 +39,28 @@ module Api
return render_unauthorized("User account is not active")
end
# If an application is specified, check authorization
if app_slug.present?
application = Application.find_by(slug: app_slug, app_type: "trusted_header", active: true)
# Check for forward auth rule authorization
# Get the forwarded host for domain matching
forwarded_host = request.headers["X-Forwarded-Host"] || request.headers["Host"]
unless application
Rails.logger.warn "ForwardAuth: Application not found or not configured for trusted_header: #{app_slug}"
return render_forbidden("Application not found or not configured")
if forwarded_host.present?
# Find matching forward auth rule for this domain
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
# Check if user is allowed to access this application
unless application.user_allowed?(user)
Rails.logger.info "ForwardAuth: User #{user.email_address} denied access to #{app_slug}"
return render_forbidden("You do not have permission to access this application")
# Check if user is allowed by this rule
unless rule.user_allowed?(user)
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 domain")
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
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
# User is authenticated and authorized
@@ -87,22 +90,8 @@ module Api
end
def extract_app_from_headers
# Try to extract application slug from forwarded headers
# This is useful when the proxy doesn't pass ?app= param
# 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
# This method is deprecated since we now use ForwardAuthRule domain matching
# Keeping it for backward compatibility but it's no longer used
nil
end

View File

@@ -16,6 +16,11 @@ class SessionsController < ApplicationController
return
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
unless user.active?
redirect_to signin_path, alert: "Your account is not active. Please contact an administrator."
@@ -26,13 +31,17 @@ class SessionsController < ApplicationController
if user.totp_enabled?
# Store user ID in session temporarily for TOTP verification
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
end
# Sign in successful
start_new_session_for user
redirect_to after_authentication_url, notice: "Signed in successfully."
redirect_to after_authentication_url, notice: "Signed in successfully.", allow_other_host: true
end
def verify_totp
@@ -57,16 +66,24 @@ class SessionsController < ApplicationController
# Try TOTP verification first
if user.verify_totp(code)
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
redirect_to after_authentication_url, notice: "Signed in successfully."
redirect_to after_authentication_url, notice: "Signed in successfully.", allow_other_host: true
return
end
# Try backup code verification
if user.verify_backup_code(code)
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
redirect_to after_authentication_url, notice: "Signed in successfully using backup code."
redirect_to after_authentication_url, notice: "Signed in successfully using backup code.", allow_other_host: true
return
end

View File

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

View File

@@ -8,6 +8,7 @@
</div>
<%= 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>
<%= label_tag :code, "Verification Code", class: "block text-sm font-medium text-gray-700" %>
<%= text_field_tag :code,