feat: SAML authentication controllers [CW-2958] (#12319)
This commit is contained in:
101
enterprise/app/builders/saml_user_builder.rb
Normal file
101
enterprise/app/builders/saml_user_builder.rb
Normal file
@@ -0,0 +1,101 @@
|
||||
class SamlUserBuilder
|
||||
def initialize(auth_hash, account_id)
|
||||
@auth_hash = auth_hash
|
||||
@account_id = account_id
|
||||
@saml_settings = AccountSamlSettings.find_by(account_id: account_id)
|
||||
end
|
||||
|
||||
def perform
|
||||
@user = find_or_create_user
|
||||
add_user_to_account if @user.persisted?
|
||||
@user
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def find_or_create_user
|
||||
user = User.from_email(auth_attribute('email'))
|
||||
|
||||
if user
|
||||
convert_existing_user_to_saml(user)
|
||||
return user
|
||||
end
|
||||
|
||||
create_user
|
||||
end
|
||||
|
||||
def convert_existing_user_to_saml(user)
|
||||
return if user.provider == 'saml'
|
||||
|
||||
user.update!(provider: 'saml')
|
||||
end
|
||||
|
||||
def create_user
|
||||
full_name = [auth_attribute('first_name'), auth_attribute('last_name')].compact.join(' ')
|
||||
fallback_name = auth_attribute('name') || auth_attribute('email').split('@').first
|
||||
|
||||
User.create(
|
||||
email: auth_attribute('email'),
|
||||
name: (full_name.presence || fallback_name),
|
||||
display_name: auth_attribute('first_name'),
|
||||
provider: 'saml',
|
||||
uid: uid,
|
||||
password: SecureRandom.hex(32),
|
||||
confirmed_at: Time.current
|
||||
)
|
||||
end
|
||||
|
||||
def add_user_to_account
|
||||
account = Account.find_by(id: @account_id)
|
||||
return unless account
|
||||
|
||||
# Create account_user if not exists
|
||||
account_user = AccountUser.find_or_create_by(
|
||||
user: @user,
|
||||
account: account
|
||||
)
|
||||
|
||||
# Set default role as agent if not set
|
||||
account_user.update(role: 'agent') if account_user.role.blank?
|
||||
|
||||
# Handle role mappings if configured
|
||||
apply_role_mappings(account_user, account)
|
||||
end
|
||||
|
||||
def apply_role_mappings(account_user, account)
|
||||
matching_mapping = find_matching_role_mapping(account)
|
||||
return unless matching_mapping
|
||||
|
||||
if matching_mapping['role']
|
||||
account_user.update(role: matching_mapping['role'])
|
||||
elsif matching_mapping['custom_role_id']
|
||||
account_user.update(custom_role_id: matching_mapping['custom_role_id'])
|
||||
end
|
||||
end
|
||||
|
||||
def find_matching_role_mapping(_account)
|
||||
return if @saml_settings&.role_mappings.blank?
|
||||
|
||||
saml_groups.each do |group|
|
||||
mapping = @saml_settings.role_mappings[group]
|
||||
return mapping if mapping.present?
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def auth_attribute(key, fallback = nil)
|
||||
@auth_hash.dig('info', key) || fallback
|
||||
end
|
||||
|
||||
def uid
|
||||
@auth_hash['uid']
|
||||
end
|
||||
|
||||
def saml_groups
|
||||
# Groups can come from different attributes depending on IdP
|
||||
@auth_hash.dig('extra', 'raw_info', 'groups') ||
|
||||
@auth_hash.dig('extra', 'raw_info', 'Group') ||
|
||||
@auth_hash.dig('extra', 'raw_info', 'memberOf') ||
|
||||
[]
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,64 @@
|
||||
module Enterprise::DeviseOverrides::OmniauthCallbacksController
|
||||
def saml
|
||||
# Call parent's omniauth_success which handles the auth
|
||||
omniauth_success
|
||||
end
|
||||
|
||||
def redirect_callbacks
|
||||
# derive target redirect route from 'resource_class' param, which was set
|
||||
# before authentication.
|
||||
devise_mapping = get_devise_mapping
|
||||
redirect_route = get_redirect_route(devise_mapping)
|
||||
|
||||
# preserve omniauth info for success route. ignore 'extra' in twitter
|
||||
# auth response to avoid CookieOverflow.
|
||||
session['dta.omniauth.auth'] = request.env['omniauth.auth'].except('extra')
|
||||
session['dta.omniauth.params'] = request.env['omniauth.params']
|
||||
|
||||
# For SAML, use 303 See Other to convert POST to GET and preserve session
|
||||
if params[:provider] == 'saml'
|
||||
redirect_to redirect_route, { status: 303 }.merge(redirect_options)
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
def omniauth_success
|
||||
case auth_hash&.dig('provider')
|
||||
when 'saml'
|
||||
handle_saml_auth
|
||||
else
|
||||
super
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def handle_saml_auth
|
||||
account_id = extract_saml_account_id
|
||||
return redirect_to login_page_url(error: 'saml-not-enabled') unless saml_enabled_for_account?(account_id)
|
||||
|
||||
@resource = SamlUserBuilder.new(auth_hash, account_id).perform
|
||||
|
||||
if @resource.persisted?
|
||||
sign_in_user
|
||||
else
|
||||
redirect_to login_page_url(error: 'saml-authentication-failed')
|
||||
end
|
||||
end
|
||||
|
||||
def extract_saml_account_id
|
||||
params[:account_id] || session[:saml_account_id] || request.env['omniauth.params']&.dig('account_id')
|
||||
end
|
||||
|
||||
def saml_enabled_for_account?(account_id)
|
||||
return false if account_id.blank?
|
||||
|
||||
account = Account.find_by(id: account_id)
|
||||
|
||||
return false if account.nil?
|
||||
return false unless account.feature_enabled?('saml')
|
||||
|
||||
AccountSamlSettings.find_by(account_id: account_id).present?
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
module Enterprise::DeviseOverrides::PasswordsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email])
|
||||
render json: {
|
||||
success: false,
|
||||
errors: [I18n.t('messages.reset_password_saml_user')]
|
||||
}, status: :forbidden
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
end
|
||||
@@ -1,4 +1,18 @@
|
||||
module Enterprise::DeviseOverrides::SessionsController
|
||||
include SamlAuthenticationHelper
|
||||
|
||||
def create
|
||||
if saml_user_attempting_password_auth?(params[:email], sso_auth_token: params[:sso_auth_token])
|
||||
render json: {
|
||||
success: false,
|
||||
errors: [I18n.t('messages.login_saml_user')]
|
||||
}, status: :unauthorized
|
||||
return
|
||||
end
|
||||
|
||||
super
|
||||
end
|
||||
|
||||
def render_create_success
|
||||
create_audit_event('sign_in')
|
||||
super
|
||||
|
||||
12
enterprise/app/helpers/saml_authentication_helper.rb
Normal file
12
enterprise/app/helpers/saml_authentication_helper.rb
Normal file
@@ -0,0 +1,12 @@
|
||||
module SamlAuthenticationHelper
|
||||
def saml_user_attempting_password_auth?(email, sso_auth_token: nil)
|
||||
return false if email.blank?
|
||||
|
||||
user = User.from_email(email)
|
||||
return false unless user&.provider == 'saml'
|
||||
|
||||
return false if sso_auth_token.present? && user.valid_sso_auth_token?(sso_auth_token)
|
||||
|
||||
true
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user