Add configurable multi-provider SSO, SSO-only mode, and JIT controls via auth.yml (#441)

* Add configuration and logic for dynamic SSO provider support and stricter JIT account creation

- Introduced `config/auth.yml` for centralized auth configuration and documentation.
- Added support for multiple SSO providers, including Google, GitHub, and OpenID Connect.
- Implemented stricter JIT SSO account creation modes (`create_and_link` vs `link_only`).
- Enabled optional restriction of JIT creation by allowed email domains.
- Enhanced OmniAuth initializer for dynamic provider setup and better configurability.
- Refined login UI to handle local login disabling and emergency super-admin override.
- Updated account creation flow to respect JIT mode and domain checks.
- Added tests for SSO account creation, login form visibility, and emergency overrides.

# Conflicts:
#	app/controllers/sessions_controller.rb

* remove non-translation

* Refactor authentication views to use translation keys and update locale files

- Extracted hardcoded strings in `oidc_accounts/link.html.erb` and `sessions/new.html.erb` into translation keys for better localization support.
- Added missing translations for English and Spanish in `sessions` and `oidc_accounts` locale files.

* Enhance OmniAuth provider configuration and refine local login override logic

- Updated OmniAuth initializer to support dynamic provider configuration with `name` and scoped parameters for Google and GitHub.
- Improved local login logic to enforce stricter handling of super-admin override when local login is disabled.
- Added test for invalid super-admin override credentials.

* Document Google sign-in configuration for local development and self-hosted environments

---------

Co-authored-by: Josh Waldrep <joshua.waldrep5+github@gmail.com>
This commit is contained in:
LPW
2025-12-23 18:15:53 -05:00
committed by GitHub
parent 8972cb59f0
commit b23711ae0d
19 changed files with 788 additions and 97 deletions

View File

@@ -13,6 +13,10 @@ class OidcAccountsController < ApplicationController
@email = @pending_auth["email"]
@user_exists = User.exists?(email: @email) if @email.present?
# Determine whether we should offer JIT account creation for this
# pending auth, based on JIT mode and allowed domains.
@allow_account_creation = !AuthConfig.jit_link_only? && AuthConfig.allowed_oidc_domain?(@email)
end
def create_link
@@ -77,10 +81,20 @@ class OidcAccountsController < ApplicationController
return
end
# Create user with a secure random password since they're using OIDC
email = @pending_auth["email"]
# Respect global JIT configuration: in link_only mode or when the email
# domain is not allowed, block JIT account creation and send the user
# back to the login page with a clear message.
unless !AuthConfig.jit_link_only? && AuthConfig.allowed_oidc_domain?(email)
redirect_to new_session_path, alert: "SSO account creation is disabled. Please contact an administrator."
return
end
# Create user with a secure random password since they're using SSO
secure_password = SecureRandom.base58(32)
@user = User.new(
email: @pending_auth["email"],
email: email,
first_name: @pending_auth["first_name"],
last_name: @pending_auth["last_name"],
password: secure_password,
@@ -92,7 +106,7 @@ class OidcAccountsController < ApplicationController
@user.role = :admin
if @user.save
# Create the OIDC identity
# Create the OIDC (or other SSO) identity
OidcIdentity.create_from_omniauth(
build_auth_hash(@pending_auth),
@user

View File

@@ -3,6 +3,7 @@ class PasswordResetsController < ApplicationController
layout "auth"
before_action :ensure_password_resets_enabled
before_action :set_user_by_token, only: %i[edit update]
def new
@@ -33,6 +34,12 @@ class PasswordResetsController < ApplicationController
private
def ensure_password_resets_enabled
return if AuthConfig.password_features_enabled?
redirect_to new_session_path, alert: t("password_resets.disabled")
end
def set_user_by_token
@user = User.find_by_token_for(:password_reset, params[:token])
redirect_to new_password_reset_path, alert: t("password_resets.update.invalid_token") unless @user.present?

View File

@@ -5,24 +5,53 @@ class SessionsController < ApplicationController
layout "auth"
def new
demo = demo_config
@prefill_demo_credentials = demo_host_match?(demo)
if @prefill_demo_credentials
@email = params[:email].presence || demo["email"]
@password = params[:password].presence || demo["password"]
else
begin
demo = Rails.application.config_for(:demo)
@prefill_demo_credentials = demo_host_match?(demo)
if @prefill_demo_credentials
@email = params[:email].presence || demo["email"]
@password = params[:password].presence || demo["password"]
else
@email = params[:email]
@password = params[:password]
end
rescue RuntimeError, Errno::ENOENT, Psych::SyntaxError
# Demo config file missing or malformed - disable demo credential prefilling
@prefill_demo_credentials = false
@email = params[:email]
@password = params[:password]
end
end
def create
if user = User.authenticate_by(email: params[:email], password: params[:password])
user = nil
if AuthConfig.local_login_enabled?
user = User.authenticate_by(email: params[:email], password: params[:password])
else
# Local login is disabled. Only allow attempts when an emergency super-admin
# override is enabled and the email belongs to a super-admin.
if AuthConfig.local_admin_override_enabled?
candidate = User.find_by(email: params[:email])
unless candidate&.super_admin?
redirect_to new_session_path, alert: t("sessions.create.local_login_disabled")
return
end
user = User.authenticate_by(email: params[:email], password: params[:password])
else
redirect_to new_session_path, alert: t("sessions.create.local_login_disabled")
return
end
end
if user
if user.otp_required?
log_super_admin_override_login(user)
session[:mfa_user_id] = user.id
redirect_to verify_mfa_path
else
log_super_admin_override_login(user)
@session = create_session_for(user)
redirect_to root_path
end
@@ -85,4 +114,20 @@ class SessionsController < ApplicationController
def set_session
@session = Current.user.sessions.find(params[:id])
end
def log_super_admin_override_login(user)
# Only log when local login is globally disabled but an emergency
# super-admin override is enabled.
return if AuthConfig.local_login_enabled?
return unless AuthConfig.local_admin_override_enabled?
return unless user&.super_admin?
Rails.logger.info("[AUTH] Super admin override login: user_id=#{user.id} email=#{user.email}")
end
def demo_host_match?(demo)
return false unless demo.present? && demo["hosts"].present?
demo["hosts"].include?(request.host)
end
end