From b5db2765c3587207e34b40d96f5f919ecf1679d6 Mon Sep 17 00:00:00 2001 From: Kevin Beyrand Date: Tue, 17 Oct 2023 13:19:58 +0200 Subject: [PATCH 01/11] Implementing SSO --- Gemfile | 3 + Gemfile.lock | 8 ++ app/controllers/application_controller.rb | 8 +- app/controllers/saml_controller.rb | 100 ++++++++++++++++++++++ app/models/user.rb | 2 +- config/application.rb | 11 ++- config/environments/development.rb | 8 +- config/initializers/devise.rb | 19 +++- config/routes.rb | 6 ++ 9 files changed, 154 insertions(+), 11 deletions(-) create mode 100644 app/controllers/saml_controller.rb diff --git a/Gemfile b/Gemfile index fb3097bc..c8c36f4d 100644 --- a/Gemfile +++ b/Gemfile @@ -64,3 +64,6 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: %i[mingw mswin x64_mingw jruby] +gem "ruby-saml", "~> 1.15" +gem "devise_saml_authenticatable", "~> 1.9" + diff --git a/Gemfile.lock b/Gemfile.lock index 9e388c3c..f803732d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -101,6 +101,9 @@ GEM devise_ldap_authenticatable (0.8.7) devise (>= 3.4.1) net-ldap (>= 0.16.0) + devise_saml_authenticatable (1.9.1) + devise (> 2.0.0) + ruby-saml (~> 1.7) digest (3.1.0) doorkeeper (5.6.6) railties (>= 5) @@ -234,6 +237,9 @@ GEM rack (>= 1.1) rubocop (>= 1.7.0, < 2.0) ruby-progressbar (1.11.0) + ruby-saml (1.16.0) + nokogiri (>= 1.13.10) + rexml ruby-vips (2.1.4) ffi (~> 1.12) ssrf_filter (1.0.8) @@ -263,6 +269,7 @@ DEPENDENCIES devise-i18n devise-security devise_ldap_authenticatable + devise_saml_authenticatable (~> 1.9) doorkeeper doorkeeper-i18n dotenv-rails @@ -276,6 +283,7 @@ DEPENDENCIES rails (~> 7.0.3) rails-i18n rubocop-rails + ruby-saml (~> 1.15) tzinfo-data RUBY VERSION diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 34c1ae63..6b5d88f9 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,8 +10,12 @@ class ApplicationController < ActionController::API end def info - client_app = Doorkeeper::Application.find_by(uid: params["client_id"], secret: params["client_secret"]) - render json: { valid: client_app.present?, auth: ENV['ENABLE_AUTHENTICATION'].present? } + client_app = Doorkeeper::Application.find_by(uid: params['client_id'], secret: params['client_secret']) + render json: { + valid: client_app.present?, + auth: ENV['ENABLE_AUTHENTICATION'].present?, + sso_enabled: ENV['ENABLE_SSO'].present? ? ActiveModel::Type::Boolean.new.cast(ENV['ENABLE_SSO']) : false + } end protected diff --git a/app/controllers/saml_controller.rb b/app/controllers/saml_controller.rb new file mode 100644 index 00000000..ca8bb572 --- /dev/null +++ b/app/controllers/saml_controller.rb @@ -0,0 +1,100 @@ +class SamlController < Doorkeeper::TokensController + # skip_before_action :doorkeeper_authorize! + + def metadata + meta = OneLogin::RubySaml::Metadata.new + render xml: meta.generate(settings, true) + end + + def sso + request = OneLogin::RubySaml::Authrequest.new + redirect_to(request.create(settings)) + end + + def consume + response = OneLogin::RubySaml::Response.new(params[:SAMLResponse], settings:) + + if response.is_valid? + email = response.name_id + session[:nameid] = response.name_id + user = User.find_by_email(email) + unless user + password = [*'0'..'9', *'a'..'z', *'A'..'Z', *'!'..'?'].sample(16).join + user = User.create!(email:, password:, password_confirmation: password) + user.is_user = true + user.save + end + sign_in(:user, user) + + doorkeeper_app = Doorkeeper::Application.first + access_token = Doorkeeper::AccessToken.find_or_create_for( + application: doorkeeper_app, + resource_owner: user.id, + scopes: Doorkeeper::OAuth::Scopes.from_array(%w[public]) + ) + + # redirect_to frontrnd + redirect_to "#{ENV['SSO_FRONTEND_REDIRECTION']}/#/?sso_token=#{access_token.token}" + else + logger.info "Response Invalid. Errors: #{response.errors}" + @errors = response.errors + redirect_to root_path + end + end + + def logout + logout_request = OneLogin::RubySaml::Logoutrequest.new + session[:transaction_id] = logout_request.uuid + + logger.info "New SP SLO for User ID: '#{session[:nameid]}', Transaction ID: '#{session[:transaction_id]}'" + + settings.name_identifier_value = session[:nameid] if settings.name_identifier_value.nil? + + redirect_to(logout_request.create(settings)) + end + + # Handle the SLO response from the IdP + # GET /saml/slo + def slo + logout_response = if session.has_key? :transaction_id + OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings, + matches_request_id: session[:transaction_id]) + else + OneLogin::RubySaml::Logoutresponse.new(params[:SAMLResponse], settings) + end + logger.info "LogoutResponse is: #{logout_response}" + + # Validate the SAML Logout Response + if !logout_response.validate + logger.error 'The SAML Logout Response is invalid' + else + # Actually log out this session + logger.info "SLO completed for '#{session[:nameid]}'" + session[:nameid] = nil + session[:transaction_id] = nil + + redirect_to ENV['SSO_FRONTEND_REDIRECTION'] + end + end + + private + + def settings + settings = OneLogin::RubySaml::Settings.new + url_base = "#{request.protocol}#{request.host_with_port}" + + # settings.soft = true + settings.issuer = "#{url_base}/saml/metadata" + settings.assertion_consumer_service_url = "#{url_base}/saml/acs" + settings.assertion_consumer_logout_service_url = "#{url_base}/saml/slo" + + # IdP section + settings.idp_entity_id = ENV['IDP_ENTITY_ID'] + settings.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL'] + settings.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL'] + settings.idp_cert = ENV['IDP_CERT'] + settings.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + + settings + end +end diff --git a/app/models/user.rb b/app/models/user.rb index f640deb8..f6cc258e 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -26,7 +26,7 @@ class User < ApplicationRecord dependent: :destroy def validate_login_uniqueness - errors.add(:login, :taken) if User.where(login: login).where.not(id: id).exists? + errors.add(:login, :taken) if User.where(login:).where.not(id:).exists? end def check_ldap_email diff --git a/config/application.rb b/config/application.rb index e8ce2cd8..f157ea32 100644 --- a/config/application.rb +++ b/config/application.rb @@ -1,4 +1,4 @@ -require_relative "boot" +require_relative 'boot' require 'rails' # Pick the frameworks you want: @@ -18,7 +18,6 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) - module PiaBack class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. @@ -38,7 +37,7 @@ class Application < Rails::Application config.api_only = true # set the default locale to French - config.i18n.default_locale = ENV.fetch("DEFAULT_LOCALE", :en) + config.i18n.default_locale = ENV.fetch('DEFAULT_LOCALE', :en) # if a locale isn't found fall back to this default locale config.i18n.fallbacks = true # set the possible locales to English and Brazilian-Portuguese @@ -50,5 +49,11 @@ class Application < Rails::Application config.action_view.sanitized_allowed_tags = tags_allowed attributes_allowed = ENV['SANITIZED_ALLOWED_ATTRIBUTES'] ? ENV['SANITIZED_ALLOWED_ATTRIBUTES'].split(' ') : [] config.action_view.sanitized_allowed_attributes = attributes_allowed + + config.middleware.use ActionDispatch::Cookies + config.middleware.use ActionDispatch::Session::CookieStore + + config.secret_key_base = Rails.application.credentials.secret_key_base + config.session_store :cookie_store, expire_after: 30.minutes end end diff --git a/config/environments/development.rb b/config/environments/development.rb index 3d6b0736..638e796d 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,4 +1,4 @@ -require "active_support/core_ext/integer/time" +require 'active_support/core_ext/integer/time' Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -19,10 +19,10 @@ # Enable/disable caching. By default caching is disabled. # Run rails dev:cache to toggle caching. - if Rails.root.join("tmp/caching-dev.txt").exist? + if Rails.root.join('tmp/caching-dev.txt').exist? config.cache_store = :memory_store config.public_file_server.headers = { - "Cache-Control" => "public, max-age=#{2.days.to_i}" + 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false @@ -53,7 +53,6 @@ # Highlight code that triggered database queries in logs. config.active_record.verbose_query_logs = true - # Raises error for missing translations. # config.i18n.raise_on_missing_translations = true @@ -62,4 +61,5 @@ # Uncomment if you wish to allow Action Cable access from any origin. # config.action_cable.disable_request_forgery_protection = true + config.hosts << ENV.fetch('RAILS_CONFIG_HOSTS', 'localhost:3000') end diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 1bfe798a..f5fdb28e 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -58,7 +58,7 @@ # session. If you need permissions, you should implement that in a before filter. # You can also supply a hash where the value is a boolean determining whether # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [:email] + config.authentication_keys = [:email] # Configure parameters from the request object used for authentication. Each entry # given should be a request method and it will automatically be passed to the @@ -320,4 +320,21 @@ # 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 + + # # config.saml_default_user_key = :email # or whatever attribute you want to use as the user identifier + # config.saml_create_user = true # Automatically create users + # config.saml_update_user = true # Update user attributes after login + # config.saml_configure do |saml| + # url_base = "#{ENV['SAML_URL_BASE']}" + # saml.issuer = "#{url_base}/saml/metadata" + # saml.assertion_consumer_service_url = "#{url_base}/saml/acs" + # saml.assertion_consumer_logout_service_url = "#{url_base}/saml/logout" + + # # IdP section + # saml.idp_entity_id = ENV['IDP_ENTITY_ID'] + # saml.idp_sso_target_url = ENV['IDP_SSO_TARGET_URL'] + # saml.idp_slo_target_url = ENV['IDP_SLO_TARGET_URL'] + # saml.idp_cert = ENV['IDP_CERT'] + # saml.name_identifier_format = 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress' + # end end diff --git a/config/routes.rb b/config/routes.rb index 42dc5691..3324e396 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -35,4 +35,10 @@ resources :knowledge_bases do resources :knowledges end + + get '/saml/metadata', to: 'saml#metadata' + get '/saml/sso', to: 'saml#sso' + get '/saml/logout', to: 'saml#logout' + post '/saml/acs', to: 'saml#consume' + get '/saml/slo', to: 'saml#slo' end From de86cd039f38a905666c6c7675f583a9639674e9 Mon Sep 17 00:00:00 2001 From: Bruno Perles Date: Thu, 26 Oct 2023 12:48:06 +0200 Subject: [PATCH 02/11] Set default to rails 7.0 and fix deprecated Pundit include --- app/controllers/application_controller.rb | 2 +- config/application.rb | 2 +- .../new_framework_defaults_5_2.rb | 38 ------ .../new_framework_defaults_6_0.rb | 45 ------- .../new_framework_defaults_7_0.rb | 117 ------------------ 5 files changed, 2 insertions(+), 202 deletions(-) delete mode 100644 config/initializers/new_framework_defaults_5_2.rb delete mode 100644 config/initializers/new_framework_defaults_6_0.rb delete mode 100644 config/initializers/new_framework_defaults_7_0.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 6b5d88f9..156d92ab 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -3,7 +3,7 @@ class ApplicationController < ActionController::API render text: exception, status: :internal_server_error end - include Pundit if ENV['ENABLE_AUTHENTICATION'].present? + include Pundit::Authorization if ENV['ENABLE_AUTHENTICATION'].present? if ENV['ENABLE_AUTHENTICATION'].present? before_action :doorkeeper_authorize!, except: %i[info check_uuid password_forgotten change_password] diff --git a/config/application.rb b/config/application.rb index f157ea32..ff23d355 100644 --- a/config/application.rb +++ b/config/application.rb @@ -21,7 +21,7 @@ module PiaBack class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. - config.load_defaults 5.0 + config.load_defaults 7.0 config.autoload_paths << "#{Rails.root}/lib" # Configuration for the application, engines, and railties goes here. # diff --git a/config/initializers/new_framework_defaults_5_2.rb b/config/initializers/new_framework_defaults_5_2.rb deleted file mode 100644 index c383d072..00000000 --- a/config/initializers/new_framework_defaults_5_2.rb +++ /dev/null @@ -1,38 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.2 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Make Active Record use stable #cache_key alongside new #cache_version method. -# This is needed for recyclable cache keys. -# Rails.application.config.active_record.cache_versioning = true - -# Use AES-256-GCM authenticated encryption for encrypted cookies. -# Also, embed cookie expiry in signed or encrypted cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 5.2. -# -# Existing cookies will be converted on read then written with the new scheme. -# Rails.application.config.action_dispatch.use_authenticated_cookie_encryption = true - -# Use AES-256-GCM authenticated encryption as default cipher for encrypting messages -# instead of AES-256-CBC, when use_authenticated_message_encryption is set to true. -# Rails.application.config.active_support.use_authenticated_message_encryption = true - -# Add default protection from forgery to ActionController::Base instead of in -# ApplicationController. -# Rails.application.config.action_controller.default_protect_from_forgery = true - -# Store boolean values are in sqlite3 databases as 1 and 0 instead of 't' and -# 'f' after migrating old data. -# Rails.application.config.active_record.sqlite3.represent_boolean_as_integer = true - -# Use SHA-1 instead of MD5 to generate non-sensitive digests, such as the ETag header. -# Rails.application.config.active_support.use_sha1_digests = true - -# Make `form_with` generate id attributes for any generated HTML tags. -# Rails.application.config.action_view.form_with_generates_ids = true diff --git a/config/initializers/new_framework_defaults_6_0.rb b/config/initializers/new_framework_defaults_6_0.rb deleted file mode 100644 index 92240ef5..00000000 --- a/config/initializers/new_framework_defaults_6_0.rb +++ /dev/null @@ -1,45 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 6.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -# Don't force requests from old versions of IE to be UTF-8 encoded. -# Rails.application.config.action_view.default_enforce_utf8 = false - -# Embed purpose and expiry metadata inside signed and encrypted -# cookies for increased security. -# -# This option is not backwards compatible with earlier Rails versions. -# It's best enabled when your entire app is migrated and stable on 6.0. -# Rails.application.config.action_dispatch.use_cookies_with_metadata = true - -# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. -# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false - -# Return false instead of self when enqueuing is aborted from a callback. -# Rails.application.config.active_job.return_false_on_aborted_enqueue = true - -# Send Active Storage analysis and purge jobs to dedicated queues. -# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis -# Rails.application.config.active_storage.queues.purge = :active_storage_purge - -# When assigning to a collection of attachments declared via `has_many_attached`, replace existing -# attachments instead of appending. Use #attach to add new attachments without replacing existing ones. -# Rails.application.config.active_storage.replace_on_assign_to_many = true - -# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. -# -# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), -# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. -# If you send mail in the background, job workers need to have a copy of -# MailDeliveryJob to ensure all delivery jobs are processed properly. -# Make sure your entire app is migrated and stable on 6.0 before using this setting. -# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" - -# Enable the same cache key to be reused when the object being cached of type -# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) -# of the relation's cache key into the cache version to support recycling cache key. -# Rails.application.config.active_record.collection_cache_versioning = true diff --git a/config/initializers/new_framework_defaults_7_0.rb b/config/initializers/new_framework_defaults_7_0.rb deleted file mode 100644 index a579326e..00000000 --- a/config/initializers/new_framework_defaults_7_0.rb +++ /dev/null @@ -1,117 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file eases your Rails 7.0 framework defaults upgrade. -# -# Uncomment each configuration one by one to switch to the new default. -# Once your application is ready to run with all new defaults, you can remove -# this file and set the `config.load_defaults` to `7.0`. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. -# https://guides.rubyonrails.org/upgrading_ruby_on_rails.html - -# `button_to` view helper will render `