From 97e97d9ca3b1232342eedc99b2dce9092991b25a Mon Sep 17 00:00:00 2001 From: William Welling Date: Tue, 25 Jun 2024 11:20:03 -0500 Subject: [PATCH] Replace LDAP with SAML authenticatable --- Gemfile | 2 +- app/models/user.rb | 2 +- config/attribute-map.yml | 3 + config/initializers/devise.rb | 101 ++++++++++++++++-- ...240625133800_add_session_index_to_users.rb | 5 + db/schema.rb | 1 + 6 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 config/attribute-map.yml create mode 100644 db/migrate/20240625133800_add_session_index_to_users.rb diff --git a/Gemfile b/Gemfile index 4e2e4f2..1f2aaf8 100644 --- a/Gemfile +++ b/Gemfile @@ -51,7 +51,7 @@ gem 'tzinfo-data' gem 'pg' -gem 'devise_ldap_authenticatable' +gem 'devise_saml_authenticatable' gem 'tinymce-rails' diff --git a/app/models/user.rb b/app/models/user.rb index a53bb1c..ea19010 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -6,7 +6,7 @@ class User < ApplicationRecord # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable - devise :invitable, :ldap_authenticatable, :rememberable, :validatable + devise :invitable, :saml_authenticatable, :rememberable, :validatable # Method added by Blacklight; Blacklight uses #to_s on your # user class to get a user-displayable login/identifier for diff --git a/config/attribute-map.yml b/config/attribute-map.yml new file mode 100644 index 0000000..d9ab02f --- /dev/null +++ b/config/attribute-map.yml @@ -0,0 +1,3 @@ +# attribute-map.yml +"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "user_name" +"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "email" diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index f001759..c5873f8 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -10,10 +10,6 @@ # Many of these configuration options can be set straight in your model. Devise.setup do |config| - config.ldap_logger = true ## Enable logging LDAP Query - config.ldap_create_user = true ## All valid AD users can log in and automatically create user records. - config.ldap_update_password = false ## Make sure that password change is not written back to AD. - config.ldap_use_admin_to_bind = true ## Use Administrator in bind at the time of LDAP authentication. config.authentication_keys = [:email] ## Key used for authentication. config.case_insensitive_keys = [:email] ## Does not distinguish case of key config.strip_whitespace_keys = [:email] ## Delete the blanks contained in the key @@ -236,7 +232,7 @@ # ==> Configuration for :validatable # Range for password length. - config.password_length = 6..128 + # config.password_length = 6..128 # Email regex used to validate email formats. It simply asserts that # one (and only one) @ exists in the given string. This is mainly @@ -282,7 +278,7 @@ # Time interval you can reset your password with a reset password key. # Don't put a too small interval or your users won't have the time to # change their passwords. - config.reset_password_within = 6.hours + # config.reset_password_within = 6.hours # When set to false, does not sign a user in automatically after their password is # reset. Defaults to true, so a user is signed in automatically after a reset. @@ -366,4 +362,97 @@ # When set to false, does not sign a user in automatically after their password is # changed. Defaults to true, so a user is signed in automatically after changing a password. # config.sign_in_after_change_password = true + + # ==> Configuration for :saml_authenticatable + + # Create user if the user does not exist. (Default is false) + # Can also accept a proc, for ex: + # Devise.saml_create_user = Proc.new do |model_class, saml_response, auth_value| + # model_class == Admin + # end + config.saml_create_user = true + + # Update the attributes of the user after a successful login. (Default is false) + # Can also accept a proc, for ex: + # Devise.saml_update_user = Proc.new do |model_class, saml_response, auth_value| + # model_class == Admin + # end + config.saml_update_user = true + + # Lambda that is called if Devise.saml_update_user and/or Devise.saml_create_user are true. + # Receives the model object, saml_response and auth_value, and defines how the object's values are + # updated with regards to the SAML response. + # config.saml_update_resource_hook = -> (user, saml_response, auth_value) { + # saml_response.attributes.resource_keys.each do |key| + # user.send "#{key}=", saml_response.attribute_value_by_resource_key(key) + # end + # + # if (Devise.saml_use_subject) + # user.send "#{Devise.saml_default_user_key}=", auth_value + # end + # + # user.save! + # } + + # Lambda that is called to resolve the saml_response and auth_value into the correct user object. + # Receives a copy of the ActiveRecord::Model, saml_response and auth_value. Is expected to return + # one instance of the provided model that is the matched account, or nil if none exists. + # config.saml_resource_locator = -> (model, saml_response, auth_value) { + # model.find_by(Devise.saml_default_user_key => auth_value) + # } + + # Set the default user key. The user will be looked up by this key. Make + # sure that the Authentication Response includes the attribute. + config.saml_default_user_key = :email + + # Optional. This stores the session index defined by the IDP during login. If provided it will be used as a salt + # for the user's session to facilitate an IDP initiated logout request. + config.saml_session_index_key = :session_index + + # You can set this value to use Subject or SAML assertion as info to which email will be compared. + # If you don't set it then email will be extracted from SAML assertion attributes. + # config.saml_use_subject = true + + # You can implement IdP settings with the options to support multiple IdPs and use the request object by setting this value to the name of a class that implements a ::settings method + # which takes an IdP entity id and a request object as arguments and returns a hash of idp settings for the corresponding IdP. + # config.idp_settings_adapter = "MyIdPSettingsAdapter" + config.idp_settings_adapter = nil + + # You provide you own method to find the idp_entity_id in a SAML message in the case of multiple IdPs + # by setting this to the name of a custom reader class, or use the default. + # config.idp_entity_id_reader = "DeviseSamlAuthenticatable::DefaultIdpEntityIdReader" + + # You can set the name of a class that takes the response for a failed SAML request and the strategy, + # and implements a #handle method. This method can then redirect the user, return error messages, etc. + # config.saml_failed_callback = "MySamlFailedCallbacksHandler" + + # You can customize the named routes generated in case of named route collisions with + # other Devise modules or libraries. Set the saml_route_helper_prefix to a string that will + # be appended to the named route. + # If saml_route_helper_prefix = 'saml' then the new_user_session route becomes new_saml_user_session + # config.saml_route_helper_prefix = 'saml' + + # You can add allowance for clock drift between the sp and idp. + # This is a time in seconds. + # config.allowed_clock_drift_in_seconds = 0 + + # In SAML responses, validate that the identity provider has included an InResponseTo + # header that matches the ID of the SAML request. (Default is false) + # config.saml_validate_in_response_to = false + + config.saml_sign_out_success_url = ENV['SAML_SP_ENTITY_ID'] || "http://localhost:3000" + + # Configure with your SAML settings (see ruby-saml's README for more information: https://github.com/onelogin/ruby-saml). + config.saml_configure do |settings| + settings.assertion_consumer_service_url = ENV['SAML_ASSERTION_CONSUMER_SERVICE_URL'] || "http://localhost:3000/users/saml/auth" + settings.assertion_consumer_service_binding = ENV['SAML_ASSERTION_CONSUMER_SERVICE_BINDING'] || "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" + settings.issuer = ENV['SAML_ISSUER'] || "http://localhost:3000/users/saml/metadata" + settings.name_identifier_format = ENV['SAML_NAME_IDENTIFIER_FORMAT'] || "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + settings.sp_entity_id = ENV['SAML_SP_ENTITY_ID'] || "http://localhost:3000" + settings.authn_context = ENV['SAML_AUTHN_CONTEXT'] || "urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" + settings.idp_slo_service_url = ENV['SAML_IDP_SLO_SERVICE_URL'] || "https://login.microsoftonline.com/68f381e3-46da-47b9-ba57-6f322b8f0da1/saml2" + settings.idp_sso_service_url = ENV['SAML_IDP_SSO_SERVICE_URL'] || "https://login.microsoftonline.com/68f381e3-46da-47b9-ba57-6f322b8f0da1/saml2" + settings.idp_cert_fingerprint = ENV['SAML_IDP_CERT_FINGERPRINT'] || "00:A1:2B:3C:44:55:6F:A7:88:CC:DD:EE:22:33:44:55:D6:77:8F:99" + settings.idp_cert_fingerprint_algorithm = ENV['SAML_IDP_CERT_FINGERPRINT_ALGORITHM'] || "http://www.w3.org/2000/09/xmldsig#sha1" + end end diff --git a/db/migrate/20240625133800_add_session_index_to_users.rb b/db/migrate/20240625133800_add_session_index_to_users.rb new file mode 100644 index 0000000..079181a --- /dev/null +++ b/db/migrate/20240625133800_add_session_index_to_users.rb @@ -0,0 +1,5 @@ +class AddSessionIndexToUsers < ActiveRecord::Migration[6.1] + def change + add_column :users, :session_index, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index c8d1e31..ba17490 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -372,6 +372,7 @@ t.integer "invited_by_id" t.integer "invitations_count", default: 0 t.string "remember_token" + t.string "session_index" t.index ["email"], name: "index_users_on_email", unique: true t.index ["invitation_token"], name: "index_users_on_invitation_token", unique: true t.index ["invitations_count"], name: "index_users_on_invitations_count"