about summary refs log tree commit diff
diff options
context:
space:
mode:
authorchandrn7 <chandrn@umich.edu>2022-03-09 06:07:35 -0500
committerGitHub <noreply@github.com>2022-03-09 12:07:35 +0100
commita6ed6845c9cab3b314ce6434b851cc507a71ee62 (patch)
tree41e0c690245d9ed71dd4edf82463281beb0a166c
parentd17fb7013116767fc5c7d5eef63218bd8c45b023 (diff)
Allow login through OpenID Connect (#16221)
* added OpenID Connect as an SSO option

* minor fixes

* added comments, removed an option that shouldn't be set

* fixed Gemfile.lock

* added newline to end of Gemfile.lock

* removed tab from Gemfile.lock

* remove chomp

* codeclimate changes and small name change to make function's purpose clearer

* codeclimate fix

* added SSO buttons to /about page

* minor refactor

* minor style change

* removed spurious change

* removed unecessary conditional from ensure_valid_username and added support for auth.info.name in user_params_from_auth

* minor changes
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock41
-rw-r--r--app/controllers/auth/omniauth_callbacks_controller.rb6
-rw-r--r--app/models/concerns/omniauthable.rb23
-rw-r--r--config/initializers/omniauth.rb41
5 files changed, 97 insertions, 15 deletions
diff --git a/Gemfile b/Gemfile
index b5d15da61..549e774f7 100644
--- a/Gemfile
+++ b/Gemfile
@@ -40,6 +40,7 @@ end
 gem 'net-ldap', '~> 0.17'
 gem 'omniauth-cas', '~> 2.0'
 gem 'omniauth-saml', '~> 1.10'
+gem 'gitlab-omniauth-openid-connect', '~>0.5.0', require: 'omniauth_openid_connect'
 gem 'omniauth', '~> 1.9'
 gem 'omniauth-rails_csrf_protection', '~> 0.1'
 
diff --git a/Gemfile.lock b/Gemfile.lock
index 87539348c..65343a465 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -68,6 +68,7 @@ GEM
       zeitwerk (~> 2.3)
     addressable (2.8.0)
       public_suffix (>= 2.0.2, < 5.0)
+    aes_key_wrap (1.1.0)
     airbrussh (1.4.0)
       sshkit (>= 1.6.1, != 1.7.0)
     android_key_attestation (0.3.0)
@@ -77,6 +78,7 @@ GEM
     ast (2.4.2)
     attr_encrypted (3.1.0)
       encryptor (~> 3.0.0)
+    attr_required (1.0.1)
     awrence (1.1.1)
     aws-eventstream (1.2.0)
     aws-partitions (1.558.0)
@@ -260,6 +262,10 @@ GEM
     fuubar (2.5.1)
       rspec-core (~> 3.0)
       ruby-progressbar (~> 1.4)
+    gitlab-omniauth-openid-connect (0.5.0)
+      addressable (~> 2.7)
+      omniauth (~> 1.9)
+      openid_connect (~> 1.2)
     globalid (1.0.0)
       activesupport (>= 5.0)
     hamlit (2.13.0)
@@ -286,6 +292,7 @@ GEM
       domain_name (~> 0.5)
     http-form_data (2.3.0)
     http_accept_language (2.1.1)
+    httpclient (2.8.3)
     httplog (1.5.0)
       rack (>= 1.0)
       rainbow (>= 2.0.0)
@@ -306,6 +313,10 @@ GEM
     jmespath (1.6.0)
     json (2.5.1)
     json-canonicalization (0.3.0)
+    json-jwt (1.13.0)
+      activesupport (>= 4.2)
+      aes_key_wrap
+      bindata
     json-ld (3.2.0)
       htmlentities (~> 4.3)
       json-canonicalization (~> 0.3)
@@ -406,6 +417,16 @@ GEM
     omniauth-saml (1.10.3)
       omniauth (~> 1.3, >= 1.3.2)
       ruby-saml (~> 1.9)
+    openid_connect (1.2.0)
+      activemodel
+      attr_required (>= 1.0.0)
+      json-jwt (>= 1.5.0)
+      rack-oauth2 (>= 1.6.1)
+      swd (>= 1.0.0)
+      tzinfo
+      validate_email
+      validate_url
+      webfinger (>= 1.0.1)
     openssl (2.2.0)
     openssl-signature_algorithm (0.4.0)
     orm_adapter (0.5.0)
@@ -449,6 +470,12 @@ GEM
       rack (>= 1.0, < 3)
     rack-cors (1.1.1)
       rack (>= 2.0.0)
+    rack-oauth2 (1.16.0)
+      activesupport
+      attr_required
+      httpclient
+      json-jwt (>= 1.11.0)
+      rack (>= 2.1.0)
     rack-proxy (0.7.0)
       rack
     rack-test (1.1.0)
@@ -608,6 +635,10 @@ GEM
     stoplight (2.2.1)
     strong_migrations (0.7.9)
       activerecord (>= 5)
+    swd (1.2.0)
+      activesupport (>= 3)
+      attr_required (>= 0.0.5)
+      httpclient (>= 2.4)
     temple (0.8.2)
     terminal-table (3.0.2)
       unicode-display_width (>= 1.1.1, < 3)
@@ -642,6 +673,12 @@ GEM
     unf_ext (0.0.8)
     unicode-display_width (2.1.0)
     uniform_notifier (1.14.2)
+    validate_email (0.1.6)
+      activemodel (>= 3.0)
+      mail (>= 2.2.5)
+    validate_url (1.0.13)
+      activemodel (>= 3.0.0)
+      public_suffix
     warden (1.2.9)
       rack (>= 2.0.9)
     webauthn (3.0.0.alpha1)
@@ -654,6 +691,9 @@ GEM
       safety_net_attestation (~> 0.4.0)
       securecompare (~> 1.0)
       tpm-key_attestation (~> 0.9.0)
+    webfinger (1.1.0)
+      activesupport
+      httpclient (>= 2.4)
     webmock (3.14.0)
       addressable (>= 2.8.0)
       crack (>= 0.3.2)
@@ -717,6 +757,7 @@ DEPENDENCIES
   fog-core (<= 2.1.0)
   fog-openstack (~> 0.3)
   fuubar (~> 2.5)
+  gitlab-omniauth-openid-connect (~> 0.5.0)
   hamlit-rails (~> 0.2)
   hiredis (~> 0.6)
   htmlentities (~> 4.3)
diff --git a/app/controllers/auth/omniauth_callbacks_controller.rb b/app/controllers/auth/omniauth_callbacks_controller.rb
index 991a50b03..f9cf6d655 100644
--- a/app/controllers/auth/omniauth_callbacks_controller.rb
+++ b/app/controllers/auth/omniauth_callbacks_controller.rb
@@ -4,8 +4,6 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
   skip_before_action :verify_authenticity_token
 
   def self.provides_callback_for(provider)
-    provider_id = provider.to_s.chomp '_oauth2'
-
     define_method provider do
       @user = User.find_for_oauth(request.env['omniauth.auth'], current_user)
 
@@ -20,7 +18,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
         )
 
         sign_in_and_redirect @user, event: :authentication
-        set_flash_message(:notice, :success, kind: provider_id.capitalize) if is_navigational_format?
+        set_flash_message(:notice, :success, kind: Devise.omniauth_configs[provider].strategy.display_name.capitalize) if is_navigational_format?
       else
         session["devise.#{provider}_data"] = request.env['omniauth.auth']
         redirect_to new_user_registration_url
@@ -33,7 +31,7 @@ class Auth::OmniauthCallbacksController < Devise::OmniauthCallbacksController
   end
 
   def after_sign_in_path_for(resource)
-    if resource.email_verified?
+    if resource.email_present?
       root_path
     else
       auth_setup_path(missing_email: '1')
diff --git a/app/models/concerns/omniauthable.rb b/app/models/concerns/omniauthable.rb
index 791a94911..a90d5d888 100644
--- a/app/models/concerns/omniauthable.rb
+++ b/app/models/concerns/omniauthable.rb
@@ -13,7 +13,7 @@ module Omniauthable
       Devise.omniauth_configs.keys
     end
 
-    def email_verified?
+    def email_present?
       email && email !~ TEMP_EMAIL_REGEX
     end
   end
@@ -40,16 +40,14 @@ module Omniauthable
     end
 
     def create_for_oauth(auth)
-      # Check if the user exists with provided email if the provider gives us a
-      # verified email.  If no verified email was provided or the user already
-      # exists, we assign a temporary email and ask the user to verify it on
+      # Check if the user exists with provided email. If no email was provided,
+      # we assign a temporary email and ask the user to verify it on
       # the next step via Auth::SetupController.show
 
       strategy          = Devise.omniauth_configs[auth.provider.to_sym].strategy
       assume_verified   = strategy&.security&.assume_email_is_verified
-      email_is_verified = auth.info.verified || auth.info.verified_email || assume_verified
+      email_is_verified = auth.info.verified || auth.info.verified_email || auth.info.email_verified || assume_verified
       email             = auth.info.verified_email || auth.info.email
-      email             = nil unless email_is_verified
 
       user = User.find_by(email: email) if email_is_verified
 
@@ -58,7 +56,7 @@ module Omniauthable
       user = User.new(user_params_from_auth(email, auth))
 
       user.account.avatar_remote_url = auth.info.image if /\A#{URI::DEFAULT_PARSER.make_regexp(%w(http https))}\z/.match?(auth.info.image)
-      user.skip_confirmation!
+      user.skip_confirmation! if email_is_verified
       user.save!
       user
     end
@@ -71,8 +69,8 @@ module Omniauthable
         agreement: true,
         external: true,
         account_attributes: {
-          username: ensure_unique_username(auth.uid),
-          display_name: auth.info.full_name || [auth.info.first_name, auth.info.last_name].join(' '),
+          username: ensure_unique_username(ensure_valid_username(auth.uid)),
+          display_name: auth.info.full_name || auth.info.name || [auth.info.first_name, auth.info.last_name].join(' '),
         },
       }
     end
@@ -88,5 +86,12 @@ module Omniauthable
 
       username
     end
+
+    def ensure_valid_username(starting_username)
+      starting_username = starting_username.split('@')[0]
+      temp_username = starting_username.gsub(/[^a-z0-9_]+/i, '')
+      validated_username = temp_username.truncate(30, omission: '')
+      validated_username
+    end
   end
 end
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index 1a041ad48..51241e546 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -8,7 +8,8 @@ Devise.setup do |config|
 
   # CAS strategy
   if ENV['CAS_ENABLED'] == 'true'
-    cas_options = options
+    cas_options = {}
+    cas_options[:display_name] = ENV['CAS_DISPLAY_NAME'] || 'cas'
     cas_options[:url] = ENV['CAS_URL'] if ENV['CAS_URL']
     cas_options[:host] = ENV['CAS_HOST'] if ENV['CAS_HOST']
     cas_options[:port] = ENV['CAS_PORT'] if ENV['CAS_PORT']
@@ -36,7 +37,8 @@ Devise.setup do |config|
 
   # SAML strategy
   if ENV['SAML_ENABLED'] == 'true'
-    saml_options = options
+    saml_options = {}
+    saml_options[:display_name] = ENV['SAML_DISPLAY_NAME'] || 'saml'
     saml_options[:assertion_consumer_service_url] = ENV['SAML_ACS_URL'] if ENV['SAML_ACS_URL']
     saml_options[:issuer] = ENV['SAML_ISSUER'] if ENV['SAML_ISSUER']
     saml_options[:idp_sso_target_url] = ENV['SAML_IDP_SSO_TARGET_URL'] if ENV['SAML_IDP_SSO_TARGET_URL']
@@ -64,4 +66,39 @@ Devise.setup do |config|
     saml_options[:allowed_clock_drift] = ENV['SAML_ALLOWED_CLOCK_DRIFT'] if ENV['SAML_ALLOWED_CLOCK_DRIFT']
     config.omniauth :saml, saml_options
   end
+
+  # OpenID Connect Strategy
+  if ENV['OIDC_ENABLED'] == 'true'
+    oidc_options = {}
+    oidc_options[:display_name] = ENV['OIDC_DISPLAY_NAME'] || 'openid_connect' #OPTIONAL
+    oidc_options[:issuer] = ENV['OIDC_ISSUER'] if ENV['OIDC_ISSUER'] #NEED
+    oidc_options[:discovery] = ENV['OIDC_DISCOVERY'] == 'true' if ENV['OIDC_DISCOVERY'] #OPTIONAL (default: false)
+    oidc_options[:client_auth_method] =  ENV['OIDC_CLIENT_AUTH_METHOD'] if ENV['OIDC_CLIENT_AUTH_METHOD'] #OPTIONAL (default: basic)
+    scope_string = ENV['OIDC_SCOPE'] if ENV['OIDC_SCOPE'] #NEED
+    scopes = scope_string.split(',')
+    oidc_options[:scope] = scopes.map { |x| x.to_sym }
+    oidc_options[:response_type] = ENV['OIDC_RESPONSE_TYPE'] if ENV['OIDC_RESPONSE_TYPE'] #OPTIONAL (default: code)
+    oidc_options[:response_mode] = ENV['OIDC_RESPONSE_MODE'] if ENV['OIDC_RESPONSE_MODE'] #OPTIONAL (default: query)
+    oidc_options[:display] = ENV['OIDC_DISPLAY'] if ENV['OIDC_DISPLAY'] #OPTIONAL (default: page)
+    oidc_options[:prompt] = ENV['OIDC_PROMPT'] if ENV['OIDC_PROMPT'] #OPTIONAL
+    oidc_options[:send_nonce] = ENV['OIDC_SEND_NONCE'] == 'true' if ENV['OIDC_SEND_NONCE'] #OPTIONAL (default: true)
+    oidc_options[:send_scope_to_token_endpoint] = ENV['OIDC_SEND_SCOPE_TO_TOKEN_ENDPOINT'] == 'true' if ENV['OIDC_SEND_SCOPE_TO_TOKEN_ENDPOINT'] #OPTIONAL (default: true)
+    oidc_options[:post_logout_redirect_uri] = ENV['OIDC_IDP_LOGOUT_REDIRECT_URI'] if ENV['OIDC_IDP_LOGOUT_REDIRECT_URI'] #OPTIONAL
+    oidc_options[:uid_field] = ENV['OIDC_UID_FIELD'] if ENV['OIDC_UID_FIELD'] #NEED
+    oidc_options[:client_options] = {}
+    oidc_options[:client_options][:identifier] = ENV['OIDC_CLIENT_ID'] if ENV['OIDC_CLIENT_ID'] #NEED
+    oidc_options[:client_options][:secret] = ENV['OIDC_CLIENT_SECRET'] if ENV['OIDC_CLIENT_SECRET'] #NEED
+    oidc_options[:client_options][:redirect_uri] = ENV['OIDC_REDIRECT_URI'] if ENV['OIDC_REDIRECT_URI'] #NEED
+    oidc_options[:client_options][:scheme] = ENV['OIDC_HTTP_SCHEME'] if ENV['OIDC_HTTP_SCHEME'] #OPTIONAL (default: https)
+    oidc_options[:client_options][:host] = ENV['OIDC_HOST'] if ENV['OIDC_HOST'] #OPTIONAL
+    oidc_options[:client_options][:port] = ENV['OIDC_PORT'] if ENV['OIDC_PORT'] #OPTIONAL
+    oidc_options[:client_options][:authorization_endpoint] = ENV['OIDC_AUTH_ENDPOINT'] if ENV['OIDC_AUTH_ENDPOINT'] #NEED when discovery != true
+    oidc_options[:client_options][:token_endpoint] = ENV['OIDC_TOKEN_ENDPOINT'] if ENV['OIDC_TOKEN_ENDPOINT'] #NEED when discovery != true
+    oidc_options[:client_options][:userinfo_endpoint] = ENV['OIDC_USER_INFO_ENDPOINT'] if ENV['OIDC_USER_INFO_ENDPOINT'] #NEED when discovery != true
+    oidc_options[:client_options][:jwks_uri] = ENV['OIDC_JWKS_URI'] if ENV['OIDC_JWKS_URI'] #NEED when discovery != true
+    oidc_options[:client_options][:end_session_endpoint] = ENV['OIDC_END_SESSION_ENDPOINT'] if ENV['OIDC_END_SESSION_ENDPOINT'] #OPTIONAL
+    oidc_options[:security] = {}
+    oidc_options[:security][:assume_email_is_verified] = ENV['OIDC_SECURITY_ASSUME_EMAIL_IS_VERIFIED'] == 'true' #OPTIONAL
+    config.omniauth :openid_connect, oidc_options
+  end
 end