mirror of
https://github.com/we-promise/sure.git
synced 2026-04-07 14:31:25 +00:00
Multi-provider SSO support: - Database-backed SSO provider management with admin UI - Support for OpenID Connect, Google OAuth2, GitHub, and SAML 2.0 - Flipper feature flag (db_sso_providers) for dynamic provider loading - ProviderLoader service for YAML or database configuration Admin functionality: - Admin::SsoProvidersController for CRUD operations - Admin::UsersController for super_admin role management - Pundit policies for authorization - Test connection endpoint for validating provider config User provisioning improvements: - JIT (just-in-time) account creation with configurable default role - Changed default JIT role from admin to member (security) - User attribute sync on each SSO login - Group/role mapping from IdP claims SSO identity management: - Settings::SsoIdentitiesController for users to manage connected accounts - Issuer validation for OIDC identities - Unlink protection when no password set Audit logging: - SsoAuditLog model tracking login, logout, link, unlink, JIT creation - Captures IP address, user agent, and metadata Advanced OIDC features: - Custom scopes per provider - Configurable prompt parameter (login, consent, select_account, none) - RP-initiated logout (federated logout to IdP) - id_token storage for logout SAML 2.0 support: - omniauth-saml gem integration - IdP metadata URL or manual configuration - Certificate and fingerprint validation - NameID format configuration
112 lines
3.7 KiB
Ruby
112 lines
3.7 KiB
Ruby
class OidcIdentity < ApplicationRecord
|
|
belongs_to :user
|
|
|
|
validates :provider, presence: true
|
|
validates :uid, presence: true, uniqueness: { scope: :provider }
|
|
validates :user_id, presence: true
|
|
|
|
# Update the last authenticated timestamp
|
|
def record_authentication!
|
|
update!(last_authenticated_at: Time.current)
|
|
end
|
|
|
|
# Sync user attributes from IdP on each login
|
|
# Updates stored identity info and syncs name to user (not email - that's identity)
|
|
def sync_user_attributes!(auth)
|
|
# Extract groups from claims (various common claim names)
|
|
groups = extract_groups(auth)
|
|
|
|
# Update stored identity info with latest from IdP
|
|
update!(info: {
|
|
email: auth.info&.email,
|
|
name: auth.info&.name,
|
|
first_name: auth.info&.first_name,
|
|
last_name: auth.info&.last_name,
|
|
groups: groups
|
|
})
|
|
|
|
# Sync name to user if provided (keep existing if IdP doesn't provide)
|
|
user.update!(
|
|
first_name: auth.info&.first_name.presence || user.first_name,
|
|
last_name: auth.info&.last_name.presence || user.last_name
|
|
)
|
|
|
|
# Apply role mapping based on group membership
|
|
apply_role_mapping!(groups)
|
|
end
|
|
|
|
# Extract groups from various common IdP claim formats
|
|
def extract_groups(auth)
|
|
# Try various common group claim locations
|
|
groups = auth.extra&.raw_info&.groups ||
|
|
auth.extra&.raw_info&.[]("groups") ||
|
|
auth.extra&.raw_info&.[]("Group") ||
|
|
auth.info&.groups ||
|
|
auth.extra&.raw_info&.[]("http://schemas.microsoft.com/ws/2008/06/identity/claims/groups") ||
|
|
auth.extra&.raw_info&.[]("cognito:groups") ||
|
|
[]
|
|
|
|
# Normalize to array of strings
|
|
Array(groups).map(&:to_s)
|
|
end
|
|
|
|
# Apply role mapping based on IdP group membership
|
|
def apply_role_mapping!(groups)
|
|
config = provider_config
|
|
return unless config.present?
|
|
|
|
role_mapping = config.dig(:settings, :role_mapping) || config.dig(:settings, "role_mapping")
|
|
return unless role_mapping.present?
|
|
|
|
# Check roles in order of precedence (highest to lowest)
|
|
%w[super_admin admin member].each do |role|
|
|
mapped_groups = role_mapping[role] || role_mapping[role.to_sym] || []
|
|
mapped_groups = Array(mapped_groups)
|
|
|
|
# Check if user is in any of the mapped groups
|
|
if mapped_groups.include?("*") || (mapped_groups & groups).any?
|
|
# Only update if different to avoid unnecessary writes
|
|
user.update!(role: role) unless user.role == role
|
|
Rails.logger.info("[SSO] Applied role mapping: user_id=#{user.id} role=#{role} groups=#{groups}")
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
# Extract and store relevant info from OmniAuth auth hash
|
|
def self.create_from_omniauth(auth, user)
|
|
# Extract issuer from OIDC auth response if available
|
|
issuer = auth.extra&.raw_info&.iss || auth.extra&.raw_info&.[]("iss")
|
|
|
|
create!(
|
|
user: user,
|
|
provider: auth.provider,
|
|
uid: auth.uid,
|
|
issuer: issuer,
|
|
info: {
|
|
email: auth.info&.email,
|
|
name: auth.info&.name,
|
|
first_name: auth.info&.first_name,
|
|
last_name: auth.info&.last_name
|
|
},
|
|
last_authenticated_at: Time.current
|
|
)
|
|
end
|
|
|
|
# Find the configured provider for this identity
|
|
def provider_config
|
|
Rails.configuration.x.auth.sso_providers&.find { |p| p[:name] == provider || p[:id] == provider }
|
|
end
|
|
|
|
# Validate that the stored issuer matches the configured provider's issuer
|
|
# Returns true if valid, false if mismatch (security concern)
|
|
def issuer_matches_config?
|
|
return true if issuer.blank? # Backward compatibility for old records
|
|
|
|
config = provider_config
|
|
return true if config.blank? || config[:issuer].blank? # No config to validate against
|
|
|
|
issuer == config[:issuer]
|
|
end
|
|
end
|