Add generic SAML SSO strategy implementation [SCI-9579] (#6775)

This commit is contained in:
Alex Kriuchykhin 2023-12-20 17:52:33 +01:00 committed by GitHub
parent dbedac3a9b
commit 3de2f6d0f1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 125 additions and 33 deletions

View file

@ -26,6 +26,7 @@ gem 'omniauth-linkedin-oauth2'
gem 'omniauth-okta', git: 'https://github.com/scinote-eln/omniauth-okta', branch: 'org_auth_server_support'
gem 'omniauth_openid_connect'
gem 'omniauth-rails_csrf_protection', '~> 1.0'
gem 'omniauth-saml'
# Gems for API implementation
gem 'active_model_serializers', '~> 0.10.7'

View file

@ -478,6 +478,9 @@ GEM
omniauth_openid_connect (0.7.1)
omniauth (>= 1.9, < 3)
openid_connect (~> 2.2)
omniauth-saml (2.1.0)
omniauth (~> 2.0)
ruby-saml (~> 1.12)
openid_connect (2.2.0)
activemodel
attr_required (>= 1.0.0)
@ -646,6 +649,9 @@ GEM
rubocop (>= 1.33.0, < 2.0)
ruby-progressbar (1.13.0)
ruby-rc4 (0.1.5)
ruby-saml (1.16.0)
nokogiri (>= 1.13.10)
rexml
ruby-vips (2.1.4)
ffi (~> 1.12)
ruby2_keywords (0.0.5)
@ -815,6 +821,7 @@ DEPENDENCIES
omniauth-linkedin-oauth2
omniauth-okta!
omniauth-rails_csrf_protection (~> 1.0)
omniauth-saml
omniauth_openid_connect
overcommit
pg (~> 1.5)

View file

@ -46,17 +46,7 @@ module Users
if user.blank?
# Create new user and identity
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
@ -146,16 +136,7 @@ module Users
user = User.find_by(email: auth.info.email.downcase)
if user.blank?
# Create new user and identity
user = User.new(full_name: auth.info.name,
initials: generate_initials(auth.info.name),
email: auth.info.email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user = create_user_from_auth(email, auth)
else
# Link to existing local account
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
@ -201,17 +182,7 @@ module Users
if user.blank?
# Create new user and identity
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
@ -237,6 +208,56 @@ module Users
end
end
def saml
auth = request.env['omniauth.auth']
settings = ApplicationSettings.instance
provider_conf = settings.values['saml']
raise StandardError, 'No matching SAML provider config found' if provider_conf.blank?
return redirect_to connected_accounts_path if current_user
email = auth.info.email
user = User.from_omniauth(auth)
# User found in database so just signing in
return sign_in_and_redirect(user) if user.present?
if email.blank?
# No email in the token so can not link or create user
error_message = I18n.t('devise.saml.errors.no_email')
return redirect_to after_omniauth_failure_path_for(resource_name)
end
user = User.find_by(email: email.downcase)
if user.blank?
user = create_user_from_auth(email, auth)
sign_in_and_redirect(user)
elsif provider_conf['auto_link_on_sign_in']
# Link to existing local account
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at) if user.confirmed_at.blank?
sign_in_and_redirect(user)
else
# Cannot do anything with it, so just return an error
error_message = I18n.t('devise.saml.errors.no_local_user_map')
redirect_to after_omniauth_failure_path_for(resource_name)
end
rescue StandardError => e
Rails.logger.error e.message
Rails.logger.error e.backtrace.join("\n")
error_message = I18n.t('devise.saml.errors.failed_to_save') if e.is_a?(ActiveRecord::RecordInvalid)
error_message ||= I18n.t('devise.saml.errors.generic')
redirect_to after_omniauth_failure_path_for(resource_name)
ensure
if error_message
set_flash_message(:alert, :failure, kind: I18n.t('devise.saml.provider_name'), reason: error_message)
else
set_flash_message(:notice, :success, kind: I18n.t('devise.saml.provider_name'))
end
end
# More info at:
# https://github.com/plataformatec/devise#omniauth
@ -273,5 +294,33 @@ module Users
initials = initials.strip.blank? ? 'PLCH' : initials[0..3]
initials
end
def create_user_from_auth(email, auth)
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user
end
def create_user_from_auth(email, auth)
full_name = "#{auth.info.first_name} #{auth.info.last_name}"
user = User.new(full_name: full_name,
initials: generate_initials(full_name),
email: email,
password: generate_user_password)
User.transaction do
user.save!
user.user_identities.create!(provider: auth.provider, uid: auth.uid)
user.update!(confirmed_at: user.created_at)
end
user
end
end
end

View file

@ -209,6 +209,10 @@ module ApplicationHelper
ApplicationSettings.instance.values['openid_connect'].present?
end
def saml_configured?
ApplicationSettings.instance.values['saml'].present?
end
def wopi_enabled?
ENV['WOPI_ENABLED'] == 'true'
end

View file

@ -55,5 +55,13 @@
<% end %>
</div>
<% end %>
<% if sso_enabled? && saml_configured? %>
<div class="azure-sign-in-actions">
<%= form_tag user_saml_omniauth_authorize_path, method: :post do %>
<%= submit_tag t('devise.sessions.new.saml_submit'), class: 'btn btn-primary' %>
<% end %>
</div>
<% end %>
<% end %>
</div>

View file

@ -162,7 +162,7 @@ class Extends
'RepositoryStatusValue' => 'status',
'RepositoryStockValue' => 'stock' }
OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect)
OMNIAUTH_PROVIDERS = %i(linkedin customazureactivedirectory okta openid_connect saml)
INITIAL_USER_OPTIONS = {}

View file

@ -87,6 +87,17 @@ OKTA_SETUP_PROC = lambda do |env|
env['omniauth.strategy'].options[:client_options] = client_options
end
SAML_SETUP_PROC = lambda do |env|
settings = ApplicationSettings.instance
provider_conf = settings.values['saml']
raise StandardError, 'No SAML config available for sign in' if provider_conf.blank?
env['omniauth.strategy'].options[:idp_sso_service_url] = provider_conf['idp_sso_service_url']
env['omniauth.strategy'].options[:idp_cert] = provider_conf['idp_cert']
env['omniauth.strategy'].options[:sp_entity_id] = provider_conf['sp_entity_id']
env['omniauth.strategy'].options[:uid_attribute] = 'uid'
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::CustomAzureActiveDirectory, setup: AZURE_SETUP_PROC
end
@ -99,4 +110,8 @@ Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::Okta, setup: OKTA_SETUP_PROC
end
Rails.application.config.middleware.use OmniAuth::Builder do
provider OmniAuth::Strategies::SAML, setup: SAML_SETUP_PROC
end
OmniAuth.config.logger = Rails.logger

View file

@ -34,6 +34,7 @@ en:
remember_me: "Remember me"
submit: "Log in"
azure_ad_submit: "Sign in with Azure AD"
saml_submit: "Sign in with SAML"
2fa:
title: "Two-factor authentication"
description: "Enter the one-time code found in your authenticator app to log in to SciNote."
@ -100,6 +101,13 @@ en:
generic: "Failed to sign in user"
no_local_user_map: "No local user record found"
failed_to_save: "Failed to create new user"
saml:
provider_name: "SAML"
sign_in_label: "Sign in with SAML"
errors:
generic: "Failed to sign in user"
no_local_user_map: "No local user record found"
failed_to_save: "Failed to create new user"
doorkeeper:
errors: