diff --git a/CHANGELOG b/CHANGELOG index c11ace750a9..6d889068f25 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -70,6 +70,7 @@ v 8.2.3 - Enable devise paranoid mode to prevent user enumeration attack - Webhook payload has an added, modified and removed properties for each commit - Fix 500 error when creating a merge request that removes a submodule + - Add CAS support (tduehr) v 8.2.2 - Fix 404 in redirection after removing a project (Stan Hu) diff --git a/Gemfile b/Gemfile index 76b4759499e..3f40dcdb063 100644 --- a/Gemfile +++ b/Gemfile @@ -23,6 +23,7 @@ gem 'devise-async', '~> 0.9.0' gem 'doorkeeper', '~> 2.2.0' gem 'omniauth', '~> 1.2.2' gem 'omniauth-bitbucket', '~> 0.0.2' +gem 'omniauth-cas3', '~> 1.1.2' gem 'omniauth-facebook', '~> 3.0.0' gem 'omniauth-github', '~> 1.1.1' gem 'omniauth-gitlab', '~> 1.0.0' diff --git a/Gemfile.lock b/Gemfile.lock index 88c7a6e3424..35abd152449 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -439,6 +439,10 @@ GEM multi_json (~> 1.7) omniauth (~> 1.1) omniauth-oauth (~> 1.0) + omniauth-cas3 (1.1.3) + addressable (~> 2.3) + nokogiri (~> 1.6.6) + omniauth (~> 1.2) omniauth-facebook (3.0.0) omniauth-oauth2 (~> 1.2) omniauth-github (1.1.2) @@ -893,6 +897,7 @@ DEPENDENCIES octokit (~> 3.7.0) omniauth (~> 1.2.2) omniauth-bitbucket (~> 0.0.2) + omniauth-cas3 (~> 1.1.2) omniauth-facebook (~> 3.0.0) omniauth-github (~> 1.1.1) omniauth-gitlab (~> 1.0.0) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d182e8eb04..01e2e7b2f98 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -10,6 +10,7 @@ class ApplicationController < ActionController::Base before_action :authenticate_user_from_token! before_action :authenticate_user! + before_action :validate_user_service_ticket! before_action :reject_blocked! before_action :check_password_expiration before_action :ldap_security_check @@ -202,6 +203,20 @@ class ApplicationController < ActionController::Base end end + def validate_user_service_ticket! + return unless signed_in? && session[:service_tickets] + + valid = session[:service_tickets].all? do |provider, ticket| + Gitlab::OAuth::Session.valid?(provider, ticket) + end + + unless valid + session[:service_tickets] = nil + sign_out current_user + redirect_to new_user_session_path + end + end + def check_password_expiration if current_user && current_user.password_expires_at && current_user.password_expires_at < Time.now && !current_user.ldap_user? redirect_to new_profile_password_path and return diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb index f809fa7500a..4cad98b8e98 100644 --- a/app/controllers/omniauth_callbacks_controller.rb +++ b/app/controllers/omniauth_callbacks_controller.rb @@ -1,6 +1,6 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController - protect_from_forgery except: [:kerberos, :saml] + protect_from_forgery except: [:kerberos, :saml, :cas3] Gitlab.config.omniauth.providers.each do |provider| define_method provider['name'] do @@ -42,6 +42,14 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController render 'errors/omniauth_error', layout: "errors", status: 422 end + def cas3 + ticket = params['ticket'] + if ticket + handle_service_ticket oauth['provider'], ticket + end + handle_omniauth + end + private def handle_omniauth @@ -84,6 +92,12 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController redirect_to new_user_session_path end + def handle_service_ticket provider, ticket + Gitlab::OAuth::Session.create provider, ticket + session[:service_tickets] ||= {} + session[:service_tickets][provider] = ticket + end + def oauth @oauth ||= request.env['omniauth.auth'] end diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example index db378118f85..fcf034d7911 100644 --- a/config/gitlab.yml.example +++ b/config/gitlab.yml.example @@ -287,6 +287,15 @@ production: &base # arguments, followed by optional 'args' which can be either a hash or an array. # Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html providers: + # See omniauth-cas3 for more configuration details + # - { name: 'cas3', + # label: 'cas3', + # args: { + # url: 'https://sso.example.com', + # disable_ssl_verification: false, + # login_url: '/cas/login', + # service_validate_url: '/cas/p3/serviceValidate', + # logout_url: '/cas/logout'} } # - { name: 'github', # app_id: 'YOUR_APP_ID', # app_secret: 'YOUR_APP_SECRET', @@ -324,6 +333,10 @@ production: &base # application_name: 'YOUR_APP_NAME', # application_password: 'YOUR_APP_PASSWORD' } } + # SSO maximum session duration in seconds. Defaults to CAS default of 8 hours. + # cas3: + # session_duration: 28800 + # Shared file storage settings shared: # path: /mnt/gitlab # Default: shared diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index d3d27c762aa..1043fc01ab1 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -126,6 +126,10 @@ Settings.omniauth['block_auto_created_users'] = true if Settings.omniauth['block Settings.omniauth['auto_link_ldap_user'] = false if Settings.omniauth['auto_link_ldap_user'].nil? Settings.omniauth['providers'] ||= [] +Settings.omniauth['cas3'] ||= Settingslogic.new({}) +Settings.omniauth.cas3['session_duration'] ||= 8.hours +Settings.omniauth['session_tickets'] ||= Settingslogic.new({}) +Settings.omniauth.session_tickets['cas3'] = 'ticket' Settings['shared'] ||= Settingslogic.new({}) Settings.shared['path'] = File.expand_path(Settings.shared['path'] || "shared", Rails.root) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 5fb43a86e13..92149826da7 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -241,6 +241,16 @@ Devise.setup do |config| # An Array from the configuration will be expanded. provider_arguments.concat provider['args'] when Hash + # Add procs for handling SLO + if provider['name'] == 'cas3' + provider['args'][:on_single_sign_out] = lambda do |request| + ticket = request.params[:session_index] + raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket) + Gitlab::OAuth::Session.destroy(:cas3, ticket) + true + end + end + # A Hash from the configuration will be passed as is. provider_arguments << provider['args'].symbolize_keys end diff --git a/doc/integration/README.md b/doc/integration/README.md index 8ea4111bc8a..6263353851f 100644 --- a/doc/integration/README.md +++ b/doc/integration/README.md @@ -9,6 +9,7 @@ See the documentation below for details on how to configure these services. - [LDAP](ldap.md) Set up sign in via LDAP - [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab, and Google via OAuth. - [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider +- [CAS](cas.md) Configure GitLab to sign in using CAS - [Slack](slack.md) Integrate with the Slack chat service - [OAuth2 provider](oauth_provider.md) OAuth2 application creation - [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages diff --git a/doc/integration/cas.md b/doc/integration/cas.md new file mode 100644 index 00000000000..e6b2071f193 --- /dev/null +++ b/doc/integration/cas.md @@ -0,0 +1,62 @@ +# CAS OmniAuth Provider + +To enable the CAS OmniAuth provider you must register your application with your CAS instance. This requires the service URL GitLab will supply to CAS. It should be something like: `https://gitlab.example.com:443/users/auth/cas3/callback?url`. By default handling for SLO is enabled, you only need to configure CAS for backchannel logout. + +1. On your GitLab server, open the configuration file. + + For omnibus package: + + ```sh + sudo editor /etc/gitlab/gitlab.rb + ``` + + For installations from source: + + ```sh + cd /home/git/gitlab + + sudo -u git -H editor config/gitlab.yml + ``` + +1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings. + +1. Add the provider configuration: + + For omnibus package: + + ```ruby + gitlab_rails['omniauth_providers'] = [ + { + name: "cas3", + label: "cas", + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + } + } + ] + ``` + + For installations from source: + + ``` + - { name: 'cas3', + label: 'cas', + args: { + url: 'CAS_SERVER', + login_url: '/CAS_PATH/login', + service_validate_url: '/CAS_PATH/p3/serviceValidate', + logout_url: '/CAS_PATH/logout'} } + ``` + +1. Change 'CAS_PATH' to the root of your CAS instance (ie. `cas`). + +1. If your CAS instance does not use default TGC lifetimes, update the `cas3.session_duration` to at least the current TGC maximum lifetime. To explicitly disable SLO, regardless of CAS settings, set this to 0. + +1. Save the configuration file. + +1. Restart GitLab for the changes to take effect. + +On the sign in page there should now be a CAS tab in the sign in form. diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb new file mode 100644 index 00000000000..f33bfd0bd0e --- /dev/null +++ b/lib/gitlab/o_auth/session.rb @@ -0,0 +1,17 @@ +module Gitlab + module OAuth + module Session + def self.create(provider, ticket) + Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration) + end + + def self.destroy(provider, ticket) + Rails.cache.delete("gitlab:#{provider}:#{ticket}") + end + + def self.valid?(provider, ticket) + Rails.cache.read("gitlab:#{provider}:#{ticket}").present? + end + end + end +end