diff --git a/Gemfile b/Gemfile index dc521b0001..ebe61a2cd9 100644 --- a/Gemfile +++ b/Gemfile @@ -26,6 +26,7 @@ gem 'jwt' gem 'mini_magick', '>= 4.9.5' gem 'nkf', '~> 0.2.0' gem 'omniauth', '~> 2.1.3' +gem 'omniauth-ldap', '>= 2.3.3' gem 'omniauth_openid_connect', '>= 0.8.0' gem 'omniauth-rails_csrf_protection', '~> 2.0.0' gem 'pagy', '~> 6.0', '>= 6.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 3dabc4e495..10150e7258 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -280,6 +280,7 @@ GEM net-imap (0.5.12) date net-protocol + net-ldap (0.18.0) net-pop (0.1.2) net-protocol net-protocol (0.2.2) @@ -298,6 +299,13 @@ GEM logger rack (>= 2.2.3) rack-protection + omniauth-ldap (2.3.3) + net-ldap (~> 0.16, < 1) + omniauth (>= 1.2, < 3) + pyu-ruby-sasl (>= 0.0.3.3, < 0.1) + rack (>= 1, < 4) + rubyntlm (~> 0.6.2, < 1) + version_gem (~> 1.1, >= 1.1.9) omniauth-rails_csrf_protection (2.0.0) actionpack (>= 4.2) omniauth (~> 2.0) @@ -334,6 +342,7 @@ GEM public_suffix (7.0.2) puma (6.5.0) nio4r (~> 2.0) + pyu-ruby-sasl (0.0.3.3) racc (1.8.1) rack (3.2.6) rack-oauth2 (2.3.0) @@ -454,6 +463,8 @@ GEM ruby-progressbar (1.13.0) ruby-vips (2.1.4) ffi (~> 1.12) + rubyntlm (0.6.5) + base64 rubyzip (2.4.1) securerandom (0.4.1) selenium-webdriver (4.8.0) @@ -497,6 +508,7 @@ GEM validate_url (1.0.15) activemodel (>= 3.0.0) public_suffix + version_gem (1.1.9) web-console (4.2.1) actionview (>= 6.0.0) activemodel (>= 6.0.0) @@ -557,6 +569,7 @@ DEPENDENCIES mini_magick (>= 4.9.5) nkf (~> 0.2.0) omniauth (~> 2.1.3) + omniauth-ldap (>= 2.3.3) omniauth-rails_csrf_protection (~> 2.0.0) omniauth_openid_connect (>= 0.8.0) pagy (~> 6.0, >= 6.0.0) diff --git a/app/controllers/api/v1/api_controller.rb b/app/controllers/api/v1/api_controller.rb index 0ebbe53fa1..e14e7ad796 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['LDAP_SERVER'].present? + else + !Tenant.exists?(name: current_provider, client_secret: 'local') + end end end end diff --git a/app/controllers/external_controller.rb b/app/controllers/external_controller.rb index 5873d1d9f2..d65b2b1844 100644 --- a/app/controllers/external_controller.rb +++ b/app/controllers/external_controller.rb @@ -101,6 +101,12 @@ def create_user redirect_to root_path(error: Rails.configuration.custom_error_msgs[:external_signup_error]) end + # GET /auth/failure + # Provide the user with a proper error message in case of external authentication failure + def auth_failure + redirect_to root_path(error: Rails.configuration.custom_error_msgs[:external_signup_error]) + end + # POST /recording_ready # Creates the recording in Greenlight using information received from BigBlueButton def recording_ready diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb index e7902b041a..a87e099441 100644 --- a/config/initializers/omniauth.rb +++ b/config/initializers/omniauth.rb @@ -17,7 +17,9 @@ # 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', '') + ldap_server = ENV.fetch('LDAP_SERVER', '') + lb = ENV.fetch('LOADBALANCER_ENDPOINT', '') if lb.present? @@ -44,9 +46,10 @@ 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? + 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, @@ -55,5 +58,16 @@ secret: ENV.fetch('OPENID_CONNECT_CLIENT_SECRET'), redirect_uri: File.join(ENV.fetch('OPENID_CONNECT_REDIRECT', ''), 'auth', 'openid_connect', 'callback') } + elsif ldap_server.present? + # LDAP + provider :ldap, + host: ldap_server, + title: ENV.fetch('LDAP_TITLE', nil), + port: ENV.fetch('LDAP_PORT', 389), + method: ENV.fetch('LDAP_METHOD', :plain), + base: ENV.fetch('LDAP_BASE', ''), + uid: ENV.fetch('LDAP_UID', ''), + bind_dn: ENV.fetch('LDAP_BIND_DN', ''), + password: ENV.fetch('LDAP_PASSWORD', nil) end end diff --git a/config/routes.rb b/config/routes.rb index 60489a3afd..67ef89525e 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -22,6 +22,8 @@ # External requests get '/auth/:provider/callback', to: 'external#create_user' + post '/auth/:provider/callback', to: 'external#create_user' + get '/auth/failure', to: 'external#auth_failure' 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..22cb36e275 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 LDAP is used (OIDC takes precedence) +const useLDAP = (process.env.LDAP_SERVER && !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': useLDAP ? `"${relativeUrlRoot}/auth/ldap"` : `"${relativeUrlRoot}/auth/openid_connect"`, }, }).then(context => { if (process.argv.includes("--watch")) { diff --git a/esbuild.mjs b/esbuild.mjs index e9aa8a45e1..7ef3662a9d 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 LDAP is used (OIDC takes precedence) +const useLDAP = (process.env.LDAP_SERVER && !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': useLDAP ? `"${relativeUrlRoot}/auth/ldap"` : `"${relativeUrlRoot}/auth/openid_connect"`, }, }); diff --git a/sample.env b/sample.env index 36f0d8f118..e4c413ff8a 100644 --- a/sample.env +++ b/sample.env @@ -55,6 +55,20 @@ REDIS_URL= # More information: https://github.com/bigbluebutton/greenlight/issues/5872 #USE_EMAIL_AS_EXTERNAL_ID_FALLBACK=true +# LDAP Login Provider +# +# You can enable LDAP authentication by providing values for the variables below. +# For some documentation, see here: https://github.com/omniauth/omniauth-ldap +# LDAP_TITLE="Example-LDAP" +# LDAP_SERVER=ldap.example.com +# LDAP_PORT=389 +# LDAP_METHOD=plain +# LDAP_UID=uid +# LDAP_BASE=dc=example,dc=com +# LDAP_AUTH=simple +# LDAP_BIND_DN=cn=admin,dc=example,dc=com +# LDAP_PASSWORD=password + # 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=