From a178a0fff5ffc172358b92a4f2d7f8ec775b3f1f Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Fri, 20 Oct 2023 11:02:26 +0200 Subject: [PATCH 1/3] implement SAML authentication --- Gemfile | 1 + Gemfile.lock | 7 + SAMLconfiguration.md | 160 ++++++++++++++++++++++ app/controllers/api/v1/api_controller.rb | 10 +- app/controllers/application_controller.rb | 6 + config/initializers/omniauth.rb | 86 ++++++++---- config/routes.rb | 1 + esbuild.dev.mjs | 4 +- esbuild.mjs | 4 +- sample.env | 31 +++++ 10 files changed, 281 insertions(+), 29 deletions(-) create mode 100644 SAMLconfiguration.md diff --git a/Gemfile b/Gemfile index 9e19b81431..a86cfc4fc1 100644 --- a/Gemfile +++ b/Gemfile @@ -27,6 +27,7 @@ gem 'mini_magick', '>= 4.9.5' gem 'omniauth', '~> 2.1.2' gem 'omniauth_openid_connect', '>= 0.6.1' gem 'omniauth-rails_csrf_protection', '~> 1.0.2' +gem 'omniauth-saml' gem 'pagy', '~> 6.0', '>= 6.0.0' gem 'pg' gem 'puma', '~> 5.6' diff --git a/Gemfile.lock b/Gemfile.lock index 86bfec749b..b522492679 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,6 +289,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) + omniauth-saml (2.1.0) + omniauth (~> 2.0) + ruby-saml (~> 1.12) omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) @@ -428,6 +431,9 @@ GEM rubocop-rspec (2.9.0) rubocop (~> 1.19) ruby-progressbar (1.13.0) + ruby-saml (1.15.0) + nokogiri (>= 1.13.10) + rexml ruby-vips (2.1.4) ffi (~> 1.12) rubyzip (2.3.2) @@ -530,6 +536,7 @@ DEPENDENCIES mini_magick (>= 4.9.5) omniauth (~> 2.1.2) omniauth-rails_csrf_protection (~> 1.0.2) + omniauth-saml omniauth_openid_connect (>= 0.6.1) pagy (~> 6.0, >= 6.0.0) pg diff --git a/SAMLconfiguration.md b/SAMLconfiguration.md new file mode 100644 index 0000000000..397b5c648b --- /dev/null +++ b/SAMLconfiguration.md @@ -0,0 +1,160 @@ +# SAML + +Greenlight is a Service Provider, that connects to IdP to get authentication. + +Unfortunately, Greenlight does not support SAML out of the box. But there is a [PR Request](https://github.com/bigbluebutton/greenlight/pull/1334) that gives needed functionality for GL2 and this is a port to GL3. + +## Useful links: +To get metadata of Greenlight use [\/auth/saml/metadata]() + +By default: +```xml + + +urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + +Required attributes + + + + + + + +``` + +To get metadata from IdP (in case of SimpleSamlPhp) use [\/authentication/saml/saml2/idp/metadata.php]() + +Example: +```xml + + + + + +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 + + + + +urn:oasis:names:tc:SAML:2.0:nameid-format:transient + + + +``` +## SAML configuration +SAML configuration requires configuration from both sides. +If you configure the URL where IdP Metadata can be retrieved in the variable SAML_IDP_METADATA, +the amount of configuration can be greatly reduced and only the first variable should be strictly required. + +1. Greenlight always requires setting the unique identifier of SP, that should be stored in IdP. +The best way is to set SAML_ISSUER variable in .env and then lookup greenlight metadata from IdP. + +2. Set SAML_IDP_URL variable. SAML_IDP_URL is the URL to which the authentication request should be sent. This would be on the identity provider. It can be found in the IdP's metadata in the tag. Get this tag from IdP metadata. + +3. Set SAML_CALLBACK_URL which is the URL on which the authentication response is expected. For example: "http:///auth/saml/callback" + +4. IDP_CERT_FINGERPRINT is the fingerprint of the certificate used by the IDP. + +5. SAML_NAME_IDENTIFIER - could get from IdP metadata. by default it is "urn:oasis:names:tc:SAML:2.0:nameid-format:transient" + +6. Set other variables that are required to map SAML response fields to the user fields in the Greenlight: + - SAML_UID_ATTRIBUTE, + - SAML_EMAIL_ATTRIBUTE, + - SAML_COMMONNAME_ATTRIBUTE, + +All required values could be found in the IdP configuration. Alternatively, you could find them in the SAML response from IdP using, for example, network activity in a developer console of a web browser or using SAML-tracer extention. To do so, sign in using SAML, copy encoded SAML Response, decode it using [decoder](https://www.samltool.com/decode.php) and map values from the response by the field names to the variables in the .env file. +SAML response example: +```xml + + http://localhost:8080/simplesaml/saml2/idp/metadata.php + + + + + + + + + + + 4h3YTNHasNgeSn0ufPicciH/2r0= + + + YhAeMinNvGlBW+Cb0vyOtkMw/Ql/MbS41Y65BsjEGxAdQ3BOc2PsUCAF8gljjRqK795KDkLFOU3ZQBvIDH1HY/zyxtv4nCesWwKHky6k+CU261oxrEl3g4Erox0bBTBfHxpScjUQeM7ANoor5kQAC1bmxUnq2W23wdnOEKrn/DGWuUEkkmibtQsSMv1z+0BV0sz0sYe5v9t/MAjJpcMKdctu7ip40kzmwFTthrIB8kYRGA6mNyT5vvYVPBat2FXqNPGwKW3g/tKsi/0ubC9TWLDWiVPkRi82hRVNTK9lkYlCtnZOKY4EQwoTzTAlNKF74AUdH+CfUtKAcWXxm2lMQw== + + + MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ== + + + + + + + + http://localhost:8080/simplesaml/saml2/idp/metadata.php + + + + + + + + + + + +adS0XQBcAPrG7TZ482tu7QDhyc= + + + yXRd0mN4LrVJ70c/C3PgeZIDRAqgVogQplUIIBJGiM5b3GJk7Fe1cw5D5UzXAurh5xWi/LBlzynRUus7xuNjezgNfIwGgBEyumc5cw6va1mPkgr1jLhBMCpf43fJbHmhgmaxAtLfbYI9tsjOutSsMkJ2U/I2e9hq7sUU2f4n1oqkEqfTPl4QiM/P7/QFcZX9rJSaaVqV5ftysVi2QYizxNroTz4JMAXOyYYNbJxXwGR6E8vscNSnnotf/r8kRyUnNPYGqTWp1qd4O98NS+ox9SMXHQNQqfYD2IBQ8E8s3P+2VB+lSVt8RiEll9Ymjr0eV3/BBM6AUbjEEPUPBUSsOQ== + + + MIIDXTCCAkWgAwIBAgIJALmVVuDWu4NYMA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwHhcNMTYxMjMxMTQzNDQ3WhcNNDgwNjI1MTQzNDQ3WjBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzUCFozgNb1h1M0jzNRSCjhOBnR+uVbVpaWfXYIR+AhWDdEe5ryY+CgavOg8bfLybyzFdehlYdDRgkedEB/GjG8aJw06l0qF4jDOAw0kEygWCu2mcH7XOxRt+YAH3TVHa/Hu1W3WjzkobqqqLQ8gkKWWM27fOgAZ6GieaJBN6VBSMMcPey3HWLBmc+TYJmv1dbaO2jHhKh8pfKw0W12VM8P1PIO8gv4Phu/uuJYieBWKixBEyy0lHjyixYFCR12xdh4CA47q958ZRGnnDUGFVE1QhgRacJCOZ9bd5t9mr8KLaVBYTCJo5ERE8jymab5dPqe5qKfJsCZiqWglbjUo9twIDAQABo1AwTjAdBgNVHQ4EFgQUxpuwcs/CYQOyui+r1G+3KxBNhxkwHwYDVR0jBBgwFoAUxpuwcs/CYQOyui+r1G+3KxBNhxkwDAYDVR0TBAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAAiWUKs/2x/viNCKi3Y6blEuCtAGhzOOZ9EjrvJ8+COH3Rag3tVBWrcBZ3/uhhPq5gy9lqw4OkvEws99/5jFsX1FJ6MKBgqfuy7yh5s1YfM0ANHYczMmYpZeAcQf2CGAaVfwTTfSlzNLsF2lW/ly7yapFzlYSJLGoVE+OHEu8g5SlNACUEfkXw+5Eghh+KzlIN7R6Q7r2ixWNFBC/jWf7NKUfJyX8qIG5md1YUeT6GBW9Bm2/1/RiO24JTaYlfLdKK9TYb8sG5B+OLab2DImG99CJ25RkAcSobWNF5zD0O6lgOo3cEdB/ksCq3hmtlC/DlLZ/D8CJ+7VuZnS1rR2naQ== + + + + + _121761ee0de5079eabcdbb30e4b1f8b78e5adf6474 + + + + + + + http://app.example.com + + + + + urn:oasis:names:tc:SAML:2.0:ac:classes:Password + + + + + 1 + + + group1 + + + user1@example.com + + + en + + + + +``` + +Further information about configuration parameters could be found in the sample.env file. diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 0ebbe53fa1..c1fb391036 100644 --- a/app/controllers/api/v1/api_controller.rb +++ b/app/controllers/api/v1/api_controller.rb @@ -89,11 +89,13 @@ def config_sorting(allowed_columns: []) { sort_column => sort_direction } end - # Checks if external authentication is enabled (currently only OIDC is implemented) + # Checks if external authentication is enabled def external_auth? - return ENV['OPENID_CONNECT_ISSUER'].present? if ENV['LOADBALANCER_ENDPOINT'].blank? - - !Tenant.exists?(name: current_provider, client_secret: 'local') + if ENV['LOADBALANCER_ENDPOINT'].blank? + ENV['OPENID_CONNECT_ISSUER'].present? || ENV['SAML_ENTITY_ID'].present? + else + !Tenant.exists?(name: current_provider, client_secret: 'local') + end end end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 69795c8d4c..36523cdea0 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -19,6 +19,12 @@ class ApplicationController < ActionController::Base include Pagy::Backend + # Disable forgery protection für SAML callback + skip_forgery_protection if: :saml_callback_path? + def saml_callback_path? + request.fullpath == '/auth/saml/callback' + end + # Returns the current signed in User (if any) def current_user return @current_user if @current_user diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index 7d0a0cb7c9..6e3323c6e6 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -17,34 +17,39 @@ # frozen_string_literal: true Rails.application.config.middleware.use OmniAuth::Builder do - issuer = ENV.fetch('OPENID_CONNECT_ISSUER', '') + oidc_issuer = ENV.fetch('OPENID_CONNECT_ISSUER', '') + saml_entity = ENV.fetch('SAML_ENTITY_ID', '') lb = ENV.fetch('LOADBALANCER_ENDPOINT', '') if lb.present? - provider :openid_connect, setup: lambda { |env| - request = Rack::Request.new(env) - current_provider = request.params['current_provider'] || request.host&.split('.')&.first - secret = Tenant.find_by(name: current_provider)&.client_secret - issuer_url = File.join issuer.to_s, "/#{current_provider}" + if oidc_issuer.present? + # OpenID Connect with LB + provider :openid_connect, setup: lambda { |env| + request = Rack::Request.new(env) + current_provider = request.params['current_provider'] || request.host&.split('.')&.first + secret = Tenant.find_by(name: current_provider)&.client_secret + issuer_url = File.join oidc_issuer.to_s, "/#{current_provider}" - env['omniauth.strategy'].options[:issuer] = issuer_url - env['omniauth.strategy'].options[:scope] = %i[openid email profile] - env['omniauth.strategy'].options[:uid_field] = ENV.fetch('OPENID_CONNECT_UID_FIELD', 'sub') - env['omniauth.strategy'].options[:discovery] = true - env['omniauth.strategy'].options[:client_options].identifier = ENV.fetch('OPENID_CONNECT_CLIENT_ID') - env['omniauth.strategy'].options[:client_options].secret = secret - env['omniauth.strategy'].options[:client_options].redirect_uri = File.join( - File.join('https://', "#{current_provider}.#{ENV.fetch('OPENID_CONNECT_REDIRECT', '')}", 'auth', 'openid_connect', 'callback') - ) - env['omniauth.strategy'].options[:client_options].authorization_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'auth') - env['omniauth.strategy'].options[:client_options].token_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'token') - env['omniauth.strategy'].options[:client_options].userinfo_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'userinfo') - env['omniauth.strategy'].options[:client_options].jwks_uri = File.join(issuer_url, 'protocol', 'openid-connect', 'certs') - env['omniauth.strategy'].options[:client_options].end_session_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'logout') - } - elsif issuer.present? + env['omniauth.strategy'].options[:issuer] = issuer_url + env['omniauth.strategy'].options[:scope] = %i[openid email profile] + env['omniauth.strategy'].options[:uid_field] = ENV.fetch('OPENID_CONNECT_UID_FIELD', 'sub') + env['omniauth.strategy'].options[:discovery] = true + env['omniauth.strategy'].options[:client_options].identifier = ENV.fetch('OPENID_CONNECT_CLIENT_ID') + env['omniauth.strategy'].options[:client_options].secret = secret + env['omniauth.strategy'].options[:client_options].redirect_uri = File.join( + File.join('https://', "#{current_provider}.#{ENV.fetch('OPENID_CONNECT_REDIRECT', '')}", 'auth', 'openid_connect', 'callback') + ) + env['omniauth.strategy'].options[:client_options].authorization_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'auth') + env['omniauth.strategy'].options[:client_options].token_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'token') + env['omniauth.strategy'].options[:client_options].userinfo_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'userinfo') + env['omniauth.strategy'].options[:client_options].jwks_uri = File.join(issuer_url, 'protocol', 'openid-connect', 'certs') + env['omniauth.strategy'].options[:client_options].end_session_endpoint = File.join(issuer_url, 'protocol', 'openid-connect', 'logout') + } + end + elsif oidc_issuer.present? + # OpenID Connect provider :openid_connect, - issuer:, + issuer: oidc_issuer, scope: %i[openid email profile], uid_field: ENV.fetch('OPENID_CONNECT_UID_FIELD', 'sub'), discovery: true, @@ -53,5 +58,40 @@ secret: ENV.fetch('OPENID_CONNECT_CLIENT_SECRET'), redirect_uri: File.join(ENV.fetch('OPENID_CONNECT_REDIRECT', ''), 'auth', 'openid_connect', 'callback') } + elsif saml_entity.present? + # SAML + saml_metadata_url = ENV.fetch('SAML_METADATA_URL', '') + if saml_metadata_url.present? + idp_metadata_parser = OneLogin::RubySaml::IdpMetadataParser.new + idp_metadata = idp_metadata_parser.parse_remote_to_hash(saml_metadata_url) + else + idp_metadata = {} + end + saml_fingerprint = ENV.fetch('SAML_IDP_CERT_FINGERPRINT', '') + + provider :saml, + # Settings that are always required + sp_entity_id: saml_entity, + # Settings that can be derived from IDP Metadata + idp_entity_id: idp_metadata[:idp_entity_id], + name_identifier_format: ENV.fetch('SAML_NAME_IDENTIFIER', nil) || idp_metadata[:name_identifier_format] || + 'urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified', + idp_sso_service_url: ENV.fetch('SAML_IDP_URL', nil) || idp_metadata[:idp_sso_service_url], + idp_slo_service_url: idp_metadata[:idp_slo_service_url], + idp_attribute_names: idp_metadata[:idp_attribute_names], + idp_cert: saml_fingerprint.present? ? nil : idp_metadata[:cert], + idp_cert_multi: saml_fingerprint.present? ? nil : idp_metadata[:idp_cert_multi], + idp_cert_fingerprint: saml_fingerprint.present? ? nil : idp_metadata[:idp_cert_fingerprint], + idp_cert_fingerprint_validator: + if saml_fingerprint.present? + ->(fp) { fp if saml_fingerprint.split(',').intersect?([fp, fp.delete(':')]) } + end, + # Optional Settings + uid_attribute: ENV.fetch('SAML_UID_ATTRIBUTE', nil), + assertion_consumer_service_url: ENV.fetch('SAML_CALLBACK_URL', nil), + attribute_statements: { + email: [ENV.fetch('SAML_EMAIL_ATTRIBUTE', nil) || 'urn:mace:dir:attribute-def:mail'], + name: [ENV.fetch('SAML_COMMONNAME_ATTRIBUTE', nil) || 'urn:mace:dir:attribute-def:cn'] + } end end diff --git a/config/routes.rb b/config/routes.rb index 94e8119910..a21f0c39eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,7 @@ # External requests get '/auth/:provider/callback', to: 'external#create_user' + post '/auth/:provider/callback', to: 'external#create_user' get '/meeting_ended', to: 'external#meeting_ended' post '/recording_ready', to: 'external#recording_ready' diff --git a/esbuild.dev.mjs b/esbuild.dev.mjs index 03e9ea90f1..577507406f 100644 --- a/esbuild.dev.mjs +++ b/esbuild.dev.mjs @@ -2,6 +2,8 @@ import * as esbuild from 'esbuild'; // Fetch 'RELATIVE_URL_ROOT' ENV variable value while removing any trailing slashes. const relativeUrlRoot = (process.env.RELATIVE_URL_ROOT || '').replace(/\/*$/, ''); +// Determine whether SAML is used (OIDC takes precedence) +const useSAML = (process.env.SAML_ENTITY_ID && !process.env.OPENID_CONNECT_ISSUER); esbuild.context({ entryPoints: ['app/javascript/main.jsx'], @@ -14,7 +16,7 @@ esbuild.context({ }, define: { 'process.env.RELATIVE_URL_ROOT': `"${relativeUrlRoot}"`, - 'process.env.OMNIAUTH_PATH': `"${relativeUrlRoot}/auth/openid_connect"`, // currently, only OIDC is implemented + 'process.env.OMNIAUTH_PATH': useSAML ? `"${relativeUrlRoot}/auth/saml"` : `"${relativeUrlRoot}/auth/openid_connect"`, }, }).then(context => { if (process.argv.includes("--watch")) { diff --git a/esbuild.mjs b/esbuild.mjs index e9aa8a45e1..f06b3a0b1e 100644 --- a/esbuild.mjs +++ b/esbuild.mjs @@ -2,6 +2,8 @@ import * as esbuild from 'esbuild'; // Fetch 'RELATIVE_URL_ROOT' ENV variable value while removing any trailing slashes. const relativeUrlRoot = (process.env.RELATIVE_URL_ROOT || '').replace(/\/*$/, ''); +// Determine whether SAML is used (OIDC takes precedence) +const useSAML = (process.env.SAML_ENTITY_ID && !process.env.OPENID_CONNECT_ISSUER); await esbuild.build({ entryPoints: ['app/javascript/main.jsx'], @@ -14,7 +16,7 @@ await esbuild.build({ }, define: { 'process.env.RELATIVE_URL_ROOT': `"${relativeUrlRoot}"`, - 'process.env.OMNIAUTH_PATH': `"${relativeUrlRoot}/auth/openid_connect"`, // currently, only OIDC is implemented + 'process.env.OMNIAUTH_PATH': useSAML ? `"${relativeUrlRoot}/auth/saml"` : `"${relativeUrlRoot}/auth/openid_connect"`, }, }); diff --git a/sample.env b/sample.env index d90f6dea9b..c57cfecb2c 100644 --- a/sample.env +++ b/sample.env @@ -51,6 +51,37 @@ REDIS_URL= # More information: https://github.com/bigbluebutton/greenlight/issues/5872 #USE_EMAIL_AS_EXTERNAL_ID_FALLBACK=true +# SAML Login Provider (optional) +# Detailed information can be found in SAMLconfiguration.md +# +# You can use SAML authentication by providing the values below. Optionally, you can specify SAML_METADATA_URL to obtain some of these values from the IDP's metadata. +# SAML_ENTITY_ID is the name of your application. Some identity providers might need this to establish the identity of the service provider requesting the login. Always required. +# The location of this SP's metadata can be used here; For example : https://bigbluebutton.yourdomain.tld/auth/saml/metadata +# SAML_IDP_URL is the URL to which the authentication request should be sent. This would be on the identity provider. Required if not provided by metadata. +# It can be found in the IDP's metadata in the tag +# SAML_IDP_CERT_FINGERPRINT is the fingerprint of the certificate used by the IDP, for example "25:72:85:66:C9:94:22:98:36:84:11:E1:88:C7:AC:40:98:F9:E7:82". +# You can get the fingerprint by downloading the IDP's certificate and running : +# openssl x509 -noout -in torproject.pem -fingerprint -sha1 +# The fingerprint is required if not provided by metadata. +# SAML_CALLBACK_URL can be provided to override the default callback URL. +# SAML_NAME_IDENTIFIER describes the format of the username required by this application. +# If you need the email address, use "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress". See http://docs.oasis-open.org/security/saml/v2.0/saml-core-2.0-os.pdf section 8.3 +# for other options. Note that the identity provider might not support all options. If not specified, the IdP is free to choose the name identifier format used in the response +# SAML_...._ATTRIBUTE : Attributes from the SAML response should be mapped to the attributes used by greenlight. The defaults are based upon https://wiki.surfnet.nl/display/surfconextdev/Attributes+in+SURFconext +# +# The information about this SP (metadata) can be found on your server http:///auth/saml/metadata +# + +SAML_METADATA_URL= +SAML_ENTITY_ID= +SAML_IDP_URL= +SAML_IDP_CERT_FINGERPRINT= +SAML_CALLBACK_URL= +SAML_NAME_IDENTIFIER= +SAML_UID_ATTRIBUTE= +SAML_EMAIL_ATTRIBUTE= +SAML_COMMONNAME_ATTRIBUTE= + # To enable hCaptcha on the user sign up and sign in, define these 2 keys # More information: https://docs.bigbluebutton.org/greenlight/v3/install/#hcaptcha-setup #HCAPTCHA_SITE_KEY= From 83ee972e55632fda856b9c8b40c11e001c767b3e Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Fri, 27 Oct 2023 15:11:14 +0200 Subject: [PATCH 2/3] add SAML to the list of matched providers during migration --- app/controllers/api/v1/migrations/external_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/api/v1/migrations/external_controller.rb b/app/controllers/api/v1/migrations/external_controller.rb index 39ab29703d..cce8c90696 100644 --- a/app/controllers/api/v1/migrations/external_controller.rb +++ b/app/controllers/api/v1/migrations/external_controller.rb @@ -81,8 +81,8 @@ def create_role def create_user user_hash = user_params.to_h - # Re-write LDAP and Google to greenlight - user_hash[:provider] = %w[greenlight ldap google openid_connect].include?(user_hash[:provider]) ? 'greenlight' : user_hash[:provider] + # Re-write list of providers to greenlight + user_hash[:provider] = %w[greenlight ldap google openid_connect saml].include?(user_hash[:provider]) ? 'greenlight' : user_hash[:provider] # Returns an error if the provider does not exist unless user_hash[:provider] == 'greenlight' || Tenant.exists?(name: user_hash[:provider]) From 8eb2638bed615706171cef86a3527cd5a676029d Mon Sep 17 00:00:00 2001 From: Jan Kessler Date: Thu, 19 Sep 2024 11:37:17 +0200 Subject: [PATCH 3/3] update ruby-saml/omniauth-saml to avoid CVE-2024-45409 --- Gemfile | 2 +- Gemfile.lock | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile b/Gemfile index a86cfc4fc1..15acdaadda 100644 --- a/Gemfile +++ b/Gemfile @@ -27,7 +27,7 @@ gem 'mini_magick', '>= 4.9.5' gem 'omniauth', '~> 2.1.2' gem 'omniauth_openid_connect', '>= 0.6.1' gem 'omniauth-rails_csrf_protection', '~> 1.0.2' -gem 'omniauth-saml' +gem 'omniauth-saml', '>= 2.2.1' gem 'pagy', '~> 6.0', '>= 6.0.0' gem 'pg' gem 'puma', '~> 5.6' diff --git a/Gemfile.lock b/Gemfile.lock index b522492679..c129efab83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -289,9 +289,9 @@ GEM omniauth-rails_csrf_protection (1.0.2) actionpack (>= 4.2) omniauth (~> 2.0) - omniauth-saml (2.1.0) - omniauth (~> 2.0) - ruby-saml (~> 1.12) + omniauth-saml (2.2.1) + omniauth (~> 2.1) + ruby-saml (~> 1.17) omniauth_openid_connect (0.7.1) omniauth (>= 1.9, < 3) openid_connect (~> 2.2) @@ -431,7 +431,7 @@ GEM rubocop-rspec (2.9.0) rubocop (~> 1.19) ruby-progressbar (1.13.0) - ruby-saml (1.15.0) + ruby-saml (1.17.0) nokogiri (>= 1.13.10) rexml ruby-vips (2.1.4) @@ -536,7 +536,7 @@ DEPENDENCIES mini_magick (>= 4.9.5) omniauth (~> 2.1.2) omniauth-rails_csrf_protection (~> 1.0.2) - omniauth-saml + omniauth-saml (>= 2.2.1) omniauth_openid_connect (>= 0.6.1) pagy (~> 6.0, >= 6.0.0) pg