Files
canine/app/controllers/accounts/oidc_controller.rb
2025-12-12 16:19:44 -08:00

111 lines
3.4 KiB
Ruby

module Accounts
class OIDCController < ApplicationController
skip_before_action :authenticate_user!
before_action :load_account
def authorize
oidc_configuration = @account.sso_provider&.configuration
unless oidc_configuration.is_a?(OIDCConfiguration)
redirect_to account_sign_in_path(@account.slug), alert: "OIDC is not configured for this account"
return
end
# Store state in session for CSRF protection
state = SecureRandom.hex(32)
session[:oidc_state] = state
# Build authorization URL
authorization_url = build_authorization_url(oidc_configuration, state)
redirect_to authorization_url, allow_other_host: true
end
def callback
# Verify state for CSRF protection
unless params[:state].present? && params[:state] == session[:oidc_state]
redirect_to root_path, alert: "Invalid state parameter"
return
end
oidc_configuration = @account.sso_provider&.configuration
unless oidc_configuration.is_a?(OIDCConfiguration)
redirect_to root_path, alert: "OIDC is not configured"
return
end
if params[:error].present?
redirect_to account_sign_in_path(@account.slug), alert: "Authentication failed: #{params[:error_description] || params[:error]}"
return
end
# Exchange code for tokens
result = OIDC::Authenticator.new(oidc_configuration).authenticate(
code: params[:code],
redirect_uri: oidc_callback_url(slug: @account.slug)
)
unless result.success?
redirect_to account_sign_in_path(@account.slug), alert: result.error_message
return
end
# Create or find user
sso_provider = @account.sso_provider
ar_result = SSO::SyncUserTeams.call(
email: result.email,
team_names: result.groups || [],
account: @account,
sso_provider: sso_provider,
uid: result.uid,
name: result.name,
create_teams: sso_provider.just_in_time_team_provisioning_mode?
)
if ar_result.failure?
redirect_to account_sign_in_path(@account.slug), alert: "Failed to create user account"
return
end
# Clear session state
session.delete(:oidc_state)
# Sign in user
sign_in(ar_result.user)
session[:account_id] = @account.id
redirect_to after_sign_in_path_for(ar_result.user), notice: "Signed in successfully"
end
private
def load_account
@account = Account.friendly.find(params[:slug])
rescue ActiveRecord::RecordNotFound
redirect_to root_path, alert: "Account not found"
end
def build_authorization_url(oidc_configuration, state)
params = {
response_type: "code",
client_id: oidc_configuration.client_id,
redirect_uri: oidc_callback_url(slug: @account.slug),
scope: oidc_configuration.scopes,
state: state
}
auth_endpoint = oidc_configuration.authorization_endpoint.presence || discover_authorization_endpoint(oidc_configuration)
"#{auth_endpoint}?#{params.to_query}"
end
def discover_authorization_endpoint(oidc_configuration)
# Fetch from OIDC discovery document
discovery_url = oidc_configuration.discovery_url
response = HTTP.get(discovery_url)
if response.status.success?
JSON.parse(response.body.to_s)["authorization_endpoint"]
else
raise "Failed to discover OIDC endpoints"
end
end
end
end