diff --git a/.gitlab/ci/rules.gitlab-ci.yml b/.gitlab/ci/rules.gitlab-ci.yml
index cb938abd8a76f799369406cdaee8d52bd3e94a0c..de2b0459dd0941ad8ba523f781326a5504946005 100644
--- a/.gitlab/ci/rules.gitlab-ci.yml
+++ b/.gitlab/ci/rules.gitlab-ci.yml
@@ -1511,13 +1511,18 @@
       changes: ["vendor/gems/ipynbdiff/**/*"]
     - <<: *if-merge-request-labels-run-all-rspec
 
+.vendor:rules:omniauth-cas3:
+  rules:
+    - <<: *if-merge-request
+      changes: ["vendor/gems/omniauth-cas3/**/*"]
+    - <<: *if-merge-request-labels-run-all-rspec
+
 .vendor:rules:omniauth_crowd:
   rules:
     - <<: *if-merge-request
       changes: ["vendor/gems/omniauth_crowd/**/*"]
     - <<: *if-merge-request-labels-run-all-rspec
 
-
 .vendor:rules:omniauth-gitlab:
   rules:
     - <<: *if-merge-request
diff --git a/.gitlab/ci/vendored-gems.gitlab-ci.yml b/.gitlab/ci/vendored-gems.gitlab-ci.yml
index 8596d770a876e91dece4204cfe832eaf43b61b62..275faa389e2c422cd912d0aad2c1594339e64fea 100644
--- a/.gitlab/ci/vendored-gems.gitlab-ci.yml
+++ b/.gitlab/ci/vendored-gems.gitlab-ci.yml
@@ -14,6 +14,14 @@ vendor ipynbdiff:
     include: vendor/gems/ipynbdiff/.gitlab-ci.yml
     strategy: depend
 
+vendor omniauth-cas3:
+  extends:
+    - .vendor:rules:omniauth-cas3
+  needs: []
+  trigger:
+    include: vendor/gems/omniauth-cas3/.gitlab-ci.yml
+    strategy: depend
+
 vendor omniauth_crowd:
   extends:
     - .vendor:rules:omniauth_crowd
diff --git a/Gemfile b/Gemfile
index e919908358d468bf7bb405795f1f7c9328293a7e..9b4340ab4553cb4bb2492367aa957e39b122da0a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -40,7 +40,7 @@ gem 'omniauth', '~> 1.8'
 gem 'omniauth-auth0', '~> 2.0.0'
 gem 'omniauth-azure-activedirectory-v2', '~> 1.0'
 gem 'omniauth-azure-oauth2', '~> 0.0.9' # Deprecated v1 version
-gem 'omniauth-cas3', '~> 1.1.4'
+gem 'omniauth-cas3', '~> 1.1.4', path: 'vendor/gems/omniauth-cas3' # See vendor/gems/omniauth-cas3/README.md
 gem 'omniauth-dingtalk-oauth2', '~> 1.0'
 gem 'omniauth-alicloud', '~> 1.0.1'
 gem 'omniauth-facebook', '~> 4.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index ff6d1839de64a9f2b7adeca7b886e61524243aee..a48db249abce3d0c9b41b27c14482bafc45c3844 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -24,6 +24,14 @@ PATH
       connection_pool (~> 2.0)
       mail (~> 2.7)
 
+PATH
+  remote: vendor/gems/omniauth-cas3
+  specs:
+    omniauth-cas3 (1.1.4)
+      addressable (~> 2.3)
+      nokogiri (~> 1.7, >= 1.7.1)
+      omniauth (~> 1.2, < 3)
+
 PATH
   remote: vendor/gems/omniauth-gitlab
   specs:
@@ -899,10 +907,6 @@ GEM
       jwt (>= 1.0, < 3.0)
       omniauth (~> 1.0)
       omniauth-oauth2 (~> 1.4)
-    omniauth-cas3 (1.1.4)
-      addressable (~> 2.3)
-      nokogiri (~> 1.7, >= 1.7.1)
-      omniauth (~> 1.2)
     omniauth-dingtalk-oauth2 (1.0.1)
       omniauth-oauth2 (~> 1.7)
     omniauth-facebook (4.0.0)
@@ -1655,7 +1659,7 @@ DEPENDENCIES
   omniauth-authentiq (~> 0.3.3)
   omniauth-azure-activedirectory-v2 (~> 1.0)
   omniauth-azure-oauth2 (~> 0.0.9)
-  omniauth-cas3 (~> 1.1.4)
+  omniauth-cas3 (~> 1.1.4)!
   omniauth-dingtalk-oauth2 (~> 1.0)
   omniauth-facebook (~> 4.0.0)
   omniauth-github (~> 1.4)
diff --git a/vendor/gems/omniauth-cas3/.gitlab-ci.yml b/vendor/gems/omniauth-cas3/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..e728d704d21c698ab2469c840ca280cc740ddc66
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/.gitlab-ci.yml
@@ -0,0 +1,28 @@
+workflow:
+  rules:
+    - if: $CI_MERGE_REQUEST_ID
+
+.rspec:
+  cache:
+    key: omniauth-cas3-ruby
+    paths:
+      - vendor/gems/omniauth-cas3/vendor/ruby
+  before_script:
+    - cd vendor/gems/omniauth-cas3
+    - ruby -v                                   # Print out ruby version for debugging
+    - gem install bundler --no-document         # Bundler is not installed with the image
+    - bundle config set --local path 'vendor'   # Install dependencies into ./vendor/ruby
+    - bundle config set with 'development'
+    - bundle config set --local frozen 'true'   # Disallow Gemfile.lock changes on CI
+    - bundle config                             # Show bundler configuration
+    - bundle install -j $(nproc)
+  script:
+    - bundle exec rspec
+
+rspec-2.7:
+  image: "ruby:2.7"
+  extends: .rspec
+
+rspec-3.0:
+  image: "ruby:3.0"
+  extends: .rspec
diff --git a/vendor/gems/omniauth-cas3/Gemfile b/vendor/gems/omniauth-cas3/Gemfile
new file mode 100644
index 0000000000000000000000000000000000000000..adc6d8b37a3be9361ce4d654751f404e77484b39
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/Gemfile
@@ -0,0 +1,4 @@
+source 'https://rubygems.org'
+
+# Specify your gem's dependencies in omniauth-cas3.gemspec
+gemspec
diff --git a/vendor/gems/omniauth-cas3/Gemfile.lock b/vendor/gems/omniauth-cas3/Gemfile.lock
new file mode 100644
index 0000000000000000000000000000000000000000..4c59eb05d50e266bd8fe3779e067d1571f0a7b12
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/Gemfile.lock
@@ -0,0 +1,64 @@
+PATH
+  remote: .
+  specs:
+    omniauth-cas3 (1.1.4)
+      addressable (~> 2.3)
+      nokogiri (~> 1.7, >= 1.7.1)
+      omniauth (~> 1.2, < 3)
+
+GEM
+  remote: https://rubygems.org/
+  specs:
+    addressable (2.8.0)
+      public_suffix (>= 2.0.2, < 5.0)
+    awesome_print (1.9.2)
+    crack (0.4.5)
+      rexml
+    diff-lcs (1.5.0)
+    hashdiff (1.0.1)
+    hashie (5.0.0)
+    mini_portile2 (2.8.0)
+    nokogiri (1.13.7)
+      mini_portile2 (~> 2.8.0)
+      racc (~> 1.4)
+    omniauth (1.9.1)
+      hashie (>= 3.4.6)
+      rack (>= 1.6.2, < 3)
+    public_suffix (4.0.7)
+    racc (1.6.0)
+    rack (2.2.4)
+    rack-test (0.8.3)
+      rack (>= 1.0, < 3)
+    rake (10.5.0)
+    rexml (3.2.5)
+    rspec (3.11.0)
+      rspec-core (~> 3.11.0)
+      rspec-expectations (~> 3.11.0)
+      rspec-mocks (~> 3.11.0)
+    rspec-core (3.11.0)
+      rspec-support (~> 3.11.0)
+    rspec-expectations (3.11.0)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.11.0)
+    rspec-mocks (3.11.1)
+      diff-lcs (>= 1.2.0, < 2.0)
+      rspec-support (~> 3.11.0)
+    rspec-support (3.11.0)
+    webmock (3.14.0)
+      addressable (>= 2.8.0)
+      crack (>= 0.3.2)
+      hashdiff (>= 0.4.0, < 2.0.0)
+
+PLATFORMS
+  ruby
+
+DEPENDENCIES
+  awesome_print
+  omniauth-cas3!
+  rack-test (~> 0.6)
+  rake (~> 10.0)
+  rspec (>= 3.4)
+  webmock
+
+BUNDLED WITH
+   2.3.18
diff --git a/vendor/gems/omniauth-cas3/LICENSE b/vendor/gems/omniauth-cas3/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..402cb6e438089febf5353ad1b035b1bbc109382d
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/LICENSE
@@ -0,0 +1,23 @@
+Copyright (c) 2011 Derek Lindahl and CustomInk, LLC
+Copyright (c) 2015 tduehr
+
+MIT License
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file
diff --git a/vendor/gems/omniauth-cas3/README.md b/vendor/gems/omniauth-cas3/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..3c66341defbc4b1bdb7cf393904b1a3e657ca1e0
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/README.md
@@ -0,0 +1,134 @@
+# OmniAuth CAS Strategy [![Gem Version][version_badge]][version] [![Build Status][travis_status]][travis]
+
+This is a fork of [omniauth-cas3](https://github.com/tduehr/omniauth-cas3) to
+support:
+
+1. OmniAuth v1 and v2. OmniAuth v2 disables GET requests by default
+   and defaults to POST. GitLab already has patched v1 to use POST,
+   but other dependencies need to be updated:
+   https://gitlab.com/gitlab-org/gitlab/-/issues/30073.
+2. We may deprecate this library entirely in the future:
+   https://gitlab.com/gitlab-org/gitlab/-/issues/366212
+
+[version_badge]: https://badge.fury.io/rb/omniauth-cas3.png
+[version]: http://badge.fury.io/rb/omniauth-cas3
+[travis]: http://travis-ci.org/tduehr/omniauth-cas3
+[travis_status]: https://secure.travis-ci.org/dlindahl/omniauth-cas3.png
+[releases]: https://github.com/tduehr/omniauth-cas3/releases
+
+This is a OmniAuth 1.0 compatible port of the previously available
+[OmniAuth CAS strategy][old_omniauth_cas] that was bundled with OmniAuth 0.3. This strategy has also been updated for CAS protocol version 3.0 and patched to deal with namespace issues.
+
+* [View the documentation][document_up]
+* [Changelog][releases]
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+    gem 'omniauth-cas3'
+
+And then execute:
+
+    $ bundle
+
+Or install it yourself as:
+
+    $ gem install omniauth-cas3
+
+## Usage
+
+Use like any other OmniAuth strategy:
+
+```ruby
+Rails.application.config.middleware.use OmniAuth::Builder do
+  provider :cas3, host: 'cas.yourdomain.com'
+end
+```
+
+### Configuration Options
+
+#### Required
+
+OmniAuth CAS requires at least one of the following two configuration options:
+
+  * `url` - Defines the URL of your CAS server (i.e. `http://example.org:8080`)
+  * `host` - Defines the host of your CAS server (i.e. `example.org`).
+
+#### Optional
+
+Other configuration options:
+
+  * `port` - The port to use for your configured CAS `host`. Optional if using `url`.
+  * `ssl` - TRUE to connect to your CAS server over SSL. Optional if using `url`.
+  * `service_validate_url` - The URL to use to validate a user. Defaults to `'/serviceValidate'`.
+  * `callback_url` - The URL custom URL path which CAS uses to call back to the service.  Defaults to `/users/auth/cas3/callback`.
+  * `logout_url` - The URL to use to logout a user. Defaults to `'/logout'`.
+  * `login_url` - Defines the URL used to prompt users for their login information. Defaults to `/login` If no `host` is configured, the host application's domain will be used.
+  * `uid_field` - The user data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
+  * `ca_path` - Optional when `ssl` is `true`. Sets path of a CA certification directory. See [Net::HTTP][net_http] for more details.
+  * `disable_ssl_verification` - Optional when `ssl` is true. Disables verification.
+  * `on_single_sign_out` - Optional. Callback used when a [CAS 3.1 Single Sign Out][sso]
+    request is received.
+  * `fetch_raw_info` - Optional. Callback used to return additional "raw" user
+    info from other sources.
+
+    ```ruby
+    provider :cas3,
+             fetch_raw_info: lambda { |strategy, options, ticket, user_info|
+               ExternalService.get(user_info[:user]).attributes
+            }
+    ```
+
+Configurable options for values returned by CAS:
+
+  * `uid_key` - The user ID data attribute to use as your user's unique identifier. Defaults to `'user'` (which usually contains the user's login name).
+  * `name_key` - The data attribute containing user first and last name.  Defaults to `'name'`.
+  * `email_key` - The data attribute containing user email address.  Defaults to `'email'`.
+  * `nickname_key` - The data attribute containing user's nickname.  Defaults to `'user'`.
+  * `first_name_key` - The data attribute containing user first name.  Defaults to `'first_name'`.
+  * `last_name_key` - The data attribute containing user last name.  Defaults to `'last_name'`.
+  * `location_key` - The data attribute containing user location/address.  Defaults to `'location'`.
+  * `image_key` - The data attribute containing user image/picture.  Defaults to `'image'`.
+  * `phone_key` - The data attribute containing user contact phone number.  Defaults to `'phone'`.
+
+## Migrating from OmniAuth 0.3
+
+Given the following OmniAuth 0.3 configuration:
+
+```ruby
+provider :CAS, cas_server: 'https://cas.example.com/cas/'
+```
+
+Your new settings should look similar to this:
+
+```ruby
+provider :cas3,
+         host:      'cas.example.com',
+         login_url: '/cas/login',
+  	     service_validate_url: '/cas/p3/serviceValidate'
+```
+
+If you encounter problems wih SSL certificates you may want to set the `ca_path` parameter or activate `disable_ssl_verification` (not recommended).
+
+## Contributing
+
+1. Fork it
+2. Create your feature branch (`git checkout -b my-new-feature`)
+3. Commit your changes (`git commit -am 'Added some feature'`)
+4. Push to the branch (`git push origin my-new-feature`)
+5. Create new Pull Request
+
+## Thanks
+
+Special thanks go out to the following people
+
+  * @dlindahl For the original work in porting this from OmniAuth 0.3
+  * Phillip Aldridge (@iterateNZ) and JB Barth (@jbbarth) for helping out with Issue #3
+  * Elber Ribeiro (@dynaum) for Ubuntu SSL configuration support
+  * @rbq for README updates and OmniAuth 0.3 migration guide
+
+[old_omniauth_cas]: https://github.com/intridea/omniauth/blob/0-3-stable/oa-enterprise/lib/omniauth/strategies/cas.rb
+[document_up]: http://tduehr.github.com/omniauth-cas3/
+[net_http]: http://ruby-doc.org/stdlib-1.9.3/libdoc/net/http/rdoc/Net/HTTP.html
+[sso]: https://wiki.jasig.org/display/CASUM/Single+Sign+Out
diff --git a/vendor/gems/omniauth-cas3/Rakefile b/vendor/gems/omniauth-cas3/Rakefile
new file mode 100644
index 0000000000000000000000000000000000000000..af92638ba1385f1a05cdcc59c687abadd6cc4d74
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/Rakefile
@@ -0,0 +1,15 @@
+#!/usr/bin/env rake
+require 'bundler/gem_tasks'
+
+require 'rspec/core/rake_task'
+desc 'Default: run specs.'
+task default: :spec
+
+desc 'Run specs'
+RSpec::Core::RakeTask.new(:spec) do |t|
+  t.rspec_opts = '--require spec_helper --color --order rand'
+end
+
+task :test do
+  fail %q{This application uses RSpec. Try running "rake spec"}
+end
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth-cas3.rb b/vendor/gems/omniauth-cas3/lib/omniauth-cas3.rb
new file mode 100644
index 0000000000000000000000000000000000000000..58509b933c8249b112b8a80c88abdf4a82c0a03c
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth-cas3.rb
@@ -0,0 +1 @@
+require 'omniauth/cas3'
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth/cas3.rb b/vendor/gems/omniauth-cas3/lib/omniauth/cas3.rb
new file mode 100644
index 0000000000000000000000000000000000000000..80460aa1f319a200f5af8217ba1001c68b4037c7
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth/cas3.rb
@@ -0,0 +1,2 @@
+require 'omniauth/cas3/version'
+require 'omniauth/strategies/cas3'
\ No newline at end of file
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth/cas3/version.rb b/vendor/gems/omniauth-cas3/lib/omniauth/cas3/version.rb
new file mode 100644
index 0000000000000000000000000000000000000000..9508dd6912588488ea50694de5fc8b7d60791a85
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth/cas3/version.rb
@@ -0,0 +1,5 @@
+module Omniauth
+  module Cas3
+    VERSION = '1.1.4'
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3.rb b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3.rb
new file mode 100644
index 0000000000000000000000000000000000000000..7271621c56444c1afdb10de7522f2ca8cb96ee7b
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3.rb
@@ -0,0 +1,222 @@
+require 'omniauth'
+require 'addressable/uri'
+
+module OmniAuth
+  module Strategies
+    class CAS3
+      include OmniAuth::Strategy
+
+      # Custom Exceptions
+      class MissingCASTicket < StandardError; end
+      class InvalidCASTicket < StandardError; end
+
+      autoload :ServiceTicketValidator, 'omniauth/strategies/cas3/service_ticket_validator'
+      autoload :LogoutRequest, 'omniauth/strategies/cas3/logout_request'
+
+      attr_accessor :raw_info
+      alias_method :user_info, :raw_info
+
+      option :name, :cas3 # Required property by OmniAuth::Strategy
+
+      option :host, nil
+      option :port, nil
+      option :path, nil
+      option :ssl,  true
+      option :service_validate_url, '/p3/serviceValidate'
+      option :login_url,            '/login'
+      option :logout_url,           '/logout'
+      option :on_single_sign_out,   Proc.new {}
+      # A Proc or lambda that returns a Hash of additional user info to be
+      # merged with the info returned by the CAS server.
+      #
+      # @param [Object] An instance of OmniAuth::Strategies::CAS for the current request
+      # @param [String] The user's Service Ticket value
+      # @param [Hash] The user info for the Service Ticket returned by the CAS server
+      #
+      # @return [Hash] Extra user info
+      option :fetch_raw_info,       Proc.new { Hash.new }
+      # Make all the keys configurable with some defaults set here
+      option :uid_field, 'user'
+      option :name_key, 'name'
+      option :email_key, 'email'
+      option :nickname_key, 'user'
+      option :first_name_key, 'first_name'
+      option :last_name_key, 'last_name'
+      option :location_key, 'location'
+      option :image_key, 'image'
+      option :phone_key, 'phone'
+
+      # As required by https://github.com/intridea/omniauth/wiki/Auth-Hash-Schema
+      AuthHashSchemaKeys = %w{name email nickname first_name last_name location image phone}
+      info do
+        prune!({
+          name: raw_info[options[:name_key].to_s],
+          email: raw_info[options[:email_key].to_s],
+          nickname: raw_info[options[:nickname_key].to_s],
+          first_name: raw_info[options[:first_name_key].to_s],
+          last_name: raw_info[options[:last_name_key].to_s],
+          location: raw_info[options[:location_key].to_s],
+          image: raw_info[options[:image_key].to_s],
+          phone: raw_info[options[:phone_key].to_s]
+        })
+      end
+
+      extra do
+        prune!(
+          raw_info.delete_if{ |k,v| AuthHashSchemaKeys.include?(k) }
+        )
+      end
+
+      uid do
+        raw_info[options[:uid_field].to_s]
+      end
+
+      credentials do
+        prune!({ ticket: @ticket })
+      end
+
+      def callback_phase
+        if on_sso_path?
+          single_sign_out_phase
+        else
+          @ticket = request.params['ticket']
+          return fail!(:no_ticket, MissingCASTicket.new('No CAS Ticket')) unless @ticket
+          fetch_raw_info(@ticket)
+          return fail!(:invalid_ticket, InvalidCASTicket.new('Invalid CAS Ticket')) if raw_info.empty?
+          super
+        end
+      end
+
+      def request_phase
+        service_url = append_params(callback_url, return_url)
+
+        [
+          302,
+          {
+            'Location' => login_url(service_url),
+            'Content-Type' => 'text/plain'
+          },
+          ["You are being redirected to CAS for sign-in."]
+        ]
+      end
+
+      def on_sso_path?
+        request.post? && request.params.has_key?('logoutRequest')
+      end
+
+      def single_sign_out_phase
+        logout_request_service.new(self, request).call(options)
+      end
+
+      # Build a CAS host with protocol and port
+      #
+      #
+      def cas_url
+        extract_url if options['url']
+        validate_cas_setup
+        @cas_url ||= begin
+          uri = Addressable::URI.new
+          uri.host = options.host
+          uri.scheme = options.ssl ? 'https' : 'http'
+          uri.port = options.port
+          uri.path = options.path
+          uri.to_s
+        end
+      end
+
+      def extract_url
+        url = Addressable::URI.parse(options.delete('url'))
+        options.merge!(
+          'host' => url.host,
+          'port' => url.port,
+          'path' => url.path,
+          'ssl' => url.scheme == 'https'
+        )
+      end
+
+      def validate_cas_setup
+        if options.host.nil? || options.login_url.nil?
+          raise ArgumentError.new(":host and :login_url MUST be provided")
+        end
+      end
+
+      # Build a service-validation URL from +service+ and +ticket+.
+      # If +service+ has a ticket param, first remove it. URL-encode
+      # +service+ and add it and the +ticket+ as paraemters to the
+      # CAS serviceValidate URL.
+      #
+      # @param [String] service the service (a.k.a. return-to) URL
+      # @param [String] ticket the ticket to validate
+      #
+      # @return [String] a URL like `http://cas.mycompany.com/serviceValidate?service=...&ticket=...`
+      def service_validate_url(service_url, ticket)
+        service_url = Addressable::URI.parse(service_url)
+        service_url.query_values = service_url.query_values.tap { |qs| qs.delete('ticket') }
+        cas_url + append_params(options.service_validate_url, {
+          service: service_url.to_s,
+          ticket: ticket
+        })
+      end
+
+      # Build a CAS login URL from +service+.
+      #
+      # @param [String] service the service (a.k.a. return-to) URL
+      #
+      # @return [String] a URL like `http://cas.mycompany.com/login?service=...`
+      def login_url(service)
+        cas_url + append_params(options.login_url, { service: service })
+      end
+
+      # Adds URL-escaped +parameters+ to +base+.
+      #
+      # @param [String] base the base URL
+      # @param [String] params the parameters to append to the URL
+      #
+      # @return [String] the new joined URL.
+      def append_params(base, params)
+        params = params.each { |k,v| v = Rack::Utils.escape(v) }
+        Addressable::URI.parse(base).tap do |base_uri|
+          base_uri.query_values = (base_uri.query_values || {}).merge(params)
+        end.to_s
+      end
+
+      # Validate the Service Ticket
+      # @return [Object] the validated Service Ticket
+      def validate_service_ticket(ticket)
+        ServiceTicketValidator.new(self, options, callback_url, ticket).call
+      end
+
+    private
+
+      def fetch_raw_info(ticket)
+        ticket_user_info = validate_service_ticket(ticket).user_info
+        custom_user_info = options.fetch_raw_info.call(self, options, ticket, ticket_user_info)
+        self.raw_info = ticket_user_info.merge(custom_user_info)
+      end
+
+      # Deletes Hash pairs with `nil` values.
+      # From https://github.com/mkdynamic/omniauth-facebook/blob/972ed5e3456bcaed7df1f55efd7c05c216c8f48e/lib/omniauth/strategies/facebook.rb#L122-127
+      def prune!(hash)
+        hash.delete_if do |_, value|
+          prune!(value) if value.is_a?(Hash)
+          value.nil? || (value.respond_to?(:empty?) && value.empty?)
+        end
+      end
+
+      def return_url
+        # If the request already has a `url` parameter, then it will already be appended to the callback URL.
+        if request.params && request.params['url']
+          {}
+        else
+          { url: request.referer }
+        end
+      end
+
+      def logout_request_service
+        LogoutRequest
+      end
+    end
+  end
+end
+
+OmniAuth.config.add_camelization 'cas3', 'CAS3'
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/logout_request.rb b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/logout_request.rb
new file mode 100644
index 0000000000000000000000000000000000000000..72978227edb330fcfd7538d43706da139536287b
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/logout_request.rb
@@ -0,0 +1,73 @@
+module OmniAuth
+  module Strategies
+    class CAS3
+      class LogoutRequest
+        def initialize(strategy, request)
+          @strategy, @request = strategy, request
+        end
+
+        def call(options = {})
+          @options = options
+
+          begin
+            result = single_sign_out_callback.call(*logout_request)
+          rescue StandardError => err
+            return @strategy.fail! :logout_request, err
+          else
+            result = [200,{},'OK'] if result == true || result.nil?
+          ensure
+            return unless result
+
+            # TODO: Why does ActionPack::Response return [status,headers,body]
+            # when Rack::Response#new wants [body,status,headers]? Additionally,
+            # why does Rack::Response differ in argument order from the usual
+            # Rack-like [status,headers,body] array?
+            return Rack::Response.new(result[2],result[0],result[1]).finish
+          end
+        end
+
+      private
+
+        def logout_request
+          @logout_request ||= begin
+            saml = parse_and_ensure_namespaces(@request.params['logoutRequest'])
+            ns = saml.collect_namespaces
+            name_id = saml.xpath('//saml:NameID', ns).text
+            sess_idx = saml.xpath('//samlp:SessionIndex', ns).text
+            inject_params(name_id:name_id, session_index:sess_idx)
+            @request
+          end
+        end
+
+        def parse_and_ensure_namespaces(logout_request_xml)
+          doc = Nokogiri.parse(logout_request_xml)
+          ns = doc.collect_namespaces
+          if ns.include?('xmlns:samlp') && ns.include?('xmlns:saml')
+            doc
+          else
+            add_namespaces(doc)
+          end
+        end
+
+        def add_namespaces(logout_request_doc)
+          root = logout_request_doc.root
+          root.add_namespace('samlp', 'urn:oasis:names:tc:SAML:2.0:protocol')
+          root.add_namespace('saml', 'urn:oasis:names:tc:SAML:2.0:assertion\\')
+
+          # In order to add namespaces properly we need to re-parse the document
+          Nokogiri.parse(logout_request_doc.to_s)
+        end
+
+        def inject_params(new_params)
+          new_params.each do |key, val|
+            @request.update_param(key, val)
+          end
+        end
+
+        def single_sign_out_callback
+          @options[:on_single_sign_out]
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/service_ticket_validator.rb b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/service_ticket_validator.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4f9a61c52169a1b68281b77e5676d80da0daffb8
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/lib/omniauth/strategies/cas3/service_ticket_validator.rb
@@ -0,0 +1,103 @@
+require 'net/http'
+require 'net/https'
+require 'nokogiri'
+
+module OmniAuth
+  module Strategies
+    class CAS3
+      class ServiceTicketValidator
+        VALIDATION_REQUEST_HEADERS = { 'Accept' => '*/*' }
+
+        # Build a validator from a +configuration+, a
+        # +return_to+ URL, and a +ticket+.
+        #
+        # @param [Hash] options the OmniAuth Strategy options
+        # @param [String] return_to_url the URL of this CAS client service
+        # @param [String] ticket the service ticket to validate
+        def initialize(strategy, options, return_to_url, ticket)
+          @options = options
+          @uri = URI.parse(strategy.service_validate_url(return_to_url, ticket))
+        end
+
+        # Executes a network request to process the CAS Service Response
+        def call
+          @response_body = get_service_response_body
+          @success_body = find_authentication_success(@response_body)
+          self
+        end
+
+        # Request validation of the ticket from the CAS server's
+        # serviceValidate (CAS 2.0) function.
+        #
+        # Swallows all XML parsing errors (and returns +nil+ in those cases).
+        #
+        # @return [Hash, nil] a user information hash if the response is valid; +nil+ otherwise.
+        #
+        # @raise any connection errors encountered.
+        def user_info
+          parse_user_info(@success_body)
+        end
+
+      private
+
+        # turns an `<cas:authenticationSuccess>` node into a Hash;
+        # returns nil if given nil
+        def parse_user_info(node)
+          return nil if node.nil?
+          {}.tap do |hash|
+            node.children.each do |e|
+              node_name = e.name.sub(/^cas:/, '')
+              unless e.kind_of?(Nokogiri::XML::Text) || node_name == 'proxies'
+                # There are no child elements
+                if e.element_children.count == 0
+                  hash[node_name] = e.content
+                elsif e.element_children.count
+                  # JASIG style extra attributes
+                  if node_name == 'attributes'
+                    hash.merge!(parse_user_info(e))
+                  else
+                    hash[node_name] = [] if hash[node_name].nil?
+                    hash[node_name].push(parse_user_info(e))
+                  end
+                end
+              end
+            end
+          end
+        end
+
+        # finds an `<cas:authenticationSuccess>` node in
+        # a `<cas:serviceResponse>` body if present; returns nil
+        # if the passed body is nil or if there is no such node.
+        def find_authentication_success(body)
+          return nil if body.nil? || body == ''
+          begin
+            doc = Nokogiri::XML(body)
+            begin
+              doc.xpath('/cas:serviceResponse/cas:authenticationSuccess')
+            rescue Nokogiri::XML::XPath::SyntaxError
+              doc.xpath('/serviceResponse/authenticationSuccess')
+            end
+          rescue Nokogiri::XML::XPath::SyntaxError
+            nil
+          end
+        end
+
+        # retrieves the `<cas:serviceResponse>` XML from the CAS server
+        def get_service_response_body
+          result = ''
+          http = Net::HTTP.new(@uri.host, @uri.port)
+          http.use_ssl = @uri.port == 443 || @uri.instance_of?(URI::HTTPS)
+          if http.use_ssl?
+            http.verify_mode = OpenSSL::SSL::VERIFY_NONE if @options.disable_ssl_verification?
+            http.ca_path = @options.ca_path
+          end
+          http.start do |c|
+            response = c.get "#{@uri.path}?#{@uri.query}", VALIDATION_REQUEST_HEADERS.dup
+            result = response.body
+          end
+          result
+        end
+      end
+    end
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec b/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec
new file mode 100644
index 0000000000000000000000000000000000000000..05b9115b5967c6ce35ad0c15e5f4de1c0cc6cdbf
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/omniauth-cas3.gemspec
@@ -0,0 +1,28 @@
+# -*- encoding: utf-8 -*-
+require File.expand_path('../lib/omniauth/cas3/version', __FILE__)
+
+Gem::Specification.new do |gem|
+  gem.authors       = ["Derek Lindahl, tduehr"]
+  gem.email         = ["td@matasano.com"]
+  gem.summary       = %q{CAS 3.0 Strategy for OmniAuth}
+  gem.description   = gem.summary
+  gem.homepage      = "https://github.com/tduehr/omniauth-cas3"
+
+  gem.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
+  gem.files         = `git ls-files`.split("\n")
+  gem.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
+  gem.name          = "omniauth-cas3"
+  gem.require_paths = ["lib"]
+  gem.version       = Omniauth::Cas3::VERSION
+
+  gem.add_dependency 'omniauth',                '~> 1.2', '< 3'
+  gem.add_dependency 'nokogiri',                '~> 1.7', '>= 1.7.1'
+  gem.add_dependency 'addressable',             '~> 2.3'
+
+  gem.add_development_dependency 'rake',        '~> 10.0'
+  gem.add_development_dependency 'webmock'
+  gem.add_development_dependency 'rspec',       '>= 3.4'
+  gem.add_development_dependency 'rack-test',   '~> 0.6'
+
+  gem.add_development_dependency 'awesome_print'
+end
diff --git a/vendor/gems/omniauth-cas3/spec/fixtures/cas_failure.xml b/vendor/gems/omniauth-cas3/spec/fixtures/cas_failure.xml
new file mode 100644
index 0000000000000000000000000000000000000000..f8238a1801437ee18ecbb3178a4eb4861c28126b
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/fixtures/cas_failure.xml
@@ -0,0 +1,4 @@
+<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+  <cas:authenticationFailure>
+  </cas:authenticationFailure>
+</cas:serviceResponse>
diff --git a/vendor/gems/omniauth-cas3/spec/fixtures/cas_success.xml b/vendor/gems/omniauth-cas3/spec/fixtures/cas_success.xml
new file mode 100644
index 0000000000000000000000000000000000000000..18904f64b35a3d7ffb16eec0d5bb66120457d1f6
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/fixtures/cas_success.xml
@@ -0,0 +1,14 @@
+<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+  <cas:authenticationSuccess>
+    <cas:user>psegel</cas:user>
+    <cas:employeeid>54</cas:employeeid>
+    <cas:first_name>P. Segel</cas:first_name>
+    <cas:first_name>Peter</cas:first_name>
+    <cas:last_name>Segel</cas:last_name>
+    <cas:email>psegel@intridea.com</cas:email>
+    <cas:location>Washington, D.C.</cas:location>
+    <cas:image>/images/user.jpg</cas:image>
+    <cas:phone>555-555-5555</cas:phone>
+    <cas:hire_date>2004-07-13</cas:hire_date>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>
diff --git a/vendor/gems/omniauth-cas3/spec/fixtures/cas_success_jasig.xml b/vendor/gems/omniauth-cas3/spec/fixtures/cas_success_jasig.xml
new file mode 100644
index 0000000000000000000000000000000000000000..72f58edfb465e128bef61dc62cf2dea2290ee516
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/fixtures/cas_success_jasig.xml
@@ -0,0 +1,16 @@
+<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
+  <cas:authenticationSuccess>
+    <cas:user>psegel</cas:user>
+    <cas:attributes>
+      <cas:employeeid>54</cas:employeeid>
+      <cas:first_name>P. Segel</cas:first_name>
+      <cas:first_name>Peter</cas:first_name>
+      <cas:last_name>Segel</cas:last_name>
+      <cas:email>psegel@intridea.com</cas:email>
+      <cas:location>Washington, D.C.</cas:location>
+      <cas:image>/images/user.jpg</cas:image>
+      <cas:phone>555-555-5555</cas:phone>
+      <cas:hire_date>2004-07-13</cas:hire_date>
+    </cas:attributes>
+  </cas:authenticationSuccess>
+</cas:serviceResponse>
diff --git a/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/logout_request_spec.rb b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/logout_request_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..4834347fa0360bee1790c054c6c28aa72a04d343
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/logout_request_spec.rb
@@ -0,0 +1,127 @@
+require 'spec_helper'
+
+describe OmniAuth::Strategies::CAS3::LogoutRequest do
+  let(:strategy) { double('strategy') }
+  let(:env) do
+    { 'rack.input' => StringIO.new('','r') }
+  end
+  let(:request) { double('request', params:params, env:env) }
+  let(:params) { { 'url' => url, 'logoutRequest' => logoutRequest } }
+  let(:url) { 'http://notes.dev/signed_in' }
+  let(:logoutRequest) do
+    %Q[
+      <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion\" ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
+        <saml:NameID>@NOT_USED@</saml:NameID>
+        <samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
+      </samlp:LogoutRequest>
+    ]
+  end
+
+  subject { described_class.new(strategy, request).call(options) }
+
+  describe 'SAML attributes' do
+    let(:callback) { Proc.new{} }
+    let(:options) do
+      { on_single_sign_out: callback }
+    end
+
+    before do
+      @rack_input = nil
+      allow(callback).to receive(:call) do |req|
+        @rack_input = req.env['rack.input'].read
+        true
+      end
+    end
+
+    it 'are parsed and injected into the Rack Request parameters', :skip => true do
+      subject
+      expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-123456-123abc456def'
+    end
+
+    it 'are parsed and injected even if saml defined inside NameID', :skip => true do
+      request.params['logoutRequest'] =
+        %Q[
+          <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="foobarbaz" Version="2.0" IssueInstant="2014-10-19T17:13:50Z">
+            <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID>
+            <samlp:SessionIndex>ST-foo-bar</samlp:SessionIndex>
+          </samlp:LogoutRequest>
+      ]
+      subject
+      expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-foo-bar'
+    end
+
+    it 'are parsed and injected even if saml and samlp namespaces not defined', :skip => true do
+      request.params['logoutRequest'] =
+        %Q[
+          <samlp:LogoutRequest ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
+            <saml:NameID>@NOT_USED@</saml:NameID>
+            <samlp:SessionIndex>ST-789000-456def789ghi</samlp:SessionIndex>
+          </samlp:LogoutRequest>
+        ]
+      subject
+      expect(@rack_input).to eq 'name_id=%40NOT_USED%40&session_index=ST-789000-456def789ghi'
+    end
+
+    context 'that raise when parsed' do
+      let(:env) { { 'rack.input' => nil } }
+
+      before do
+        allow(strategy).to receive(:fail!)
+        subject
+        expect(strategy).to have_received(:fail!)
+      end
+
+      it 'responds with an error', skip: true do
+        expect(strategy).to have_received(:fail!)
+      end
+    end
+  end
+
+  describe 'with a configured callback' do
+    let(:options) do
+      { on_single_sign_out: callback }
+    end
+
+    context 'that returns TRUE' do
+      let(:callback) { Proc.new{true} }
+
+      it 'responds with OK', skip: true do
+        expect(subject[0]).to eq 200
+        expect(subject[2].body).to eq ['OK']
+      end
+    end
+
+    context 'that returns Nil' do
+      let(:callback) { Proc.new{} }
+
+      it 'responds with OK', skip: true do
+        expect(subject[0]).to eq 200
+        expect(subject[2].body).to eq ['OK']
+      end
+    end
+
+    context 'that returns a tuple' do
+      let(:callback) { Proc.new{ [400,{},'Bad Request'] } }
+
+      it 'responds with OK', skip: true do
+        expect(subject[0]).to eq 400
+        expect(subject[2].body).to eq ['Bad Request']
+      end
+    end
+
+    context 'that raises an error' do
+      let(:exception) { RuntimeError.new('error' )}
+      let(:callback) { Proc.new{raise exception} }
+
+      before do
+        allow(strategy).to receive(:fail!)
+        subject
+      end
+
+      it 'responds with an error', skip: true do
+        expect(strategy).to have_received(:fail!)
+          .with(:logout_request, exception)
+      end
+    end
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..b031d1d68fc3bc5bb1082989593ec047afe9f8be
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3/service_ticket_validator_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe OmniAuth::Strategies::CAS3::ServiceTicketValidator do
+  let(:strategy) do
+    double('strategy',
+      service_validate_url: 'https://example.org/serviceValidate'
+    )
+  end
+  let(:provider_options) do
+    double('provider_options',
+      disable_ssl_verification?: false,
+      ca_path: '/etc/ssl/certsZOMG'
+    )
+  end
+  let(:validator) do
+    OmniAuth::Strategies::CAS3::ServiceTicketValidator.new( strategy, provider_options, '/foo', nil )
+  end
+
+  describe '#call' do
+    before do
+      stub_request(:get, 'https://example.org/serviceValidate?')
+        .to_return(status: 200, body: '')
+    end
+
+    subject { validator.call }
+
+    it 'returns itself' do
+      expect(subject).to eq validator
+    end
+
+    it 'uses the configured CA path' do
+      subject
+      expect(provider_options).to have_received :ca_path
+    end
+  end
+
+  describe '#user_info' do
+    let(:ok_fixture) do
+      File.expand_path(File.join(File.dirname(__FILE__), '../../../fixtures/cas_success.xml'))
+    end
+    let(:service_response) { File.read(ok_fixture) }
+
+    before do
+      stub_request(:get, 'https://example.org/serviceValidate?')
+        .to_return(status: 200, body:service_response)
+      validator.call
+    end
+
+    subject { validator.user_info }
+
+    it 'parses user info from the response' do
+      expect(subject).to include 'user' => 'psegel'
+    end
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3_spec.rb b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3_spec.rb
new file mode 100644
index 0000000000000000000000000000000000000000..fd61fc795802f7e1d7286c1e8a58a18030892349
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/omniauth/strategies/cas3_spec.rb
@@ -0,0 +1,250 @@
+require 'spec_helper'
+
+describe OmniAuth::Strategies::CAS3, type: :strategy do
+  include Rack::Test::Methods
+
+  let(:my_cas_provider) { Class.new(OmniAuth::Strategies::CAS3) }
+  before do
+    stub_const 'MyCasProvider', my_cas_provider
+  end
+  let(:app) do
+    Rack::Builder.new {
+      use OmniAuth::Test::PhonySession
+      use MyCasProvider, name: :cas3, host: 'cas.example.org', ssl: false, port: 8080, uid_field: :employeeid
+      run lambda { |env| [404, {'Content-Type' => 'text/plain'}, [env.key?('omniauth.auth').to_s]] }
+    }.to_app
+  end
+
+  # TODO: Verify that these are even useful tests
+  shared_examples_for 'a CAS redirect response' do
+    let(:redirect_params) { 'service=' + Rack::Utils.escape("http://example.org/auth/cas3/callback?url=#{Rack::Utils.escape(return_url)}") }
+
+    before { get url, nil, request_env }
+
+    subject { last_response }
+
+    it { should be_redirect }
+
+    it 'redirects to the CAS server' do
+      expect(subject.headers).to include 'Location' => "http://cas.example.org:8080/login?#{redirect_params}"
+    end
+  end
+
+  describe '#cas_url' do
+    let(:params) { Hash.new }
+    let(:provider) { MyCasProvider.new(nil, params) }
+
+    subject { provider.cas_url }
+
+    it 'raises an ArgumentError' do
+      expect{subject}.to raise_error ArgumentError, %r{:host and :login_url MUST be provided}
+    end
+
+    context 'with an explicit :url option' do
+      let(:url) { 'https://example.org:8080/my_cas' }
+      let(:params) { super().merge url:url }
+
+      before { subject }
+
+      it { should eq url }
+
+      it 'parses the URL into it the appropriate strategy options' do
+        expect(provider.options).to include ssl:true
+        expect(provider.options).to include host:'example.org'
+        expect(provider.options).to include port:8080
+        expect(provider.options).to include path:'/my_cas'
+      end
+    end
+
+    context 'with explicit URL component' do
+      let(:params) { super().merge host:'example.org', port:1234, ssl:true, path:'/a/path' }
+
+      before { subject }
+
+      it { should eq 'https://example.org:1234/a/path' }
+
+      it 'parses the URL into it the appropriate strategy options' do
+        expect(provider.options).to include ssl:true
+        expect(provider.options).to include host:'example.org'
+        expect(provider.options).to include port:1234
+        expect(provider.options).to include path:'/a/path'
+      end
+    end
+  end
+
+  describe 'defaults' do
+    subject { MyCasProvider.default_options.to_hash }
+
+    it { should include('ssl' => true) }
+  end
+
+  describe 'GET /auth/cas3' do
+    let(:return_url) { 'http://myapp.com/admin/foo' }
+
+    context 'with a referer' do
+      let(:url) { '/auth/cas3' }
+
+      let(:request_env) { { 'HTTP_REFERER' => return_url } }
+
+      it_behaves_like 'a CAS redirect response'
+    end
+
+    context 'with an explicit return URL' do
+      let(:url) { "/auth/cas3?url=#{return_url}" }
+
+      let(:request_env) { {} }
+
+      it_behaves_like 'a CAS redirect response'
+    end
+  end
+
+  describe 'GET /auth/cas3/callback' do
+    context 'without a ticket' do
+      before { get '/auth/cas3/callback' }
+
+      subject { last_response }
+
+      it { should be_redirect }
+
+      it 'redirects with a failure message' do
+        expect(subject.headers).to include 'Location' => '/auth/failure?message=no_ticket&strategy=cas3'
+      end
+    end
+
+    context 'with an invalid ticket' do
+      before do
+        stub_request(:get, /^http:\/\/cas.example.org:8080?\/p3\/serviceValidate\?([^&]+&)?ticket=9391d/).
+           to_return( body: File.read('spec/fixtures/cas_failure.xml') )
+        get '/auth/cas3/callback?ticket=9391d'
+      end
+
+      subject { last_response }
+
+      it { should be_redirect }
+
+      it 'redirects with a failure message' do
+        expect(subject.headers).to include 'Location' => '/auth/failure?message=invalid_ticket&strategy=cas3'
+      end
+    end
+
+    describe 'with a valid ticket' do
+      shared_examples :successful_validation do
+        before do
+          stub_request(:get, /^http:\/\/cas.example.org:8080?\/p3\/serviceValidate\?([^&]+&)?ticket=593af/)
+            .with { |request| @request_uri = request.uri.to_s }
+            .to_return( body: File.read("spec/fixtures/#{xml_file_name}") )
+
+          get "/auth/cas3/callback?ticket=593af&url=#{return_url}"
+        end
+
+        it 'strips the ticket parameter from the callback URL' do
+          expect(@request_uri.scan('ticket=').size).to eq 1
+        end
+
+        it 'properly encodes the service URL' do
+          expect(WebMock).to have_requested(:get, 'http://cas.example.org:8080/p3/serviceValidate')
+            .with(query: {
+              ticket:  '593af',
+              service: 'http://example.org/auth/cas3/callback?url=' + Rack::Utils.escape('http://127.0.0.10/?some=parameter')
+            })
+        end
+
+        context "request.env['omniauth.auth']" do
+          subject { last_request.env['omniauth.auth'] }
+
+          it { should be_kind_of Hash }
+
+          it 'identifes the provider' do
+            expect(subject.provider).to eq :cas3
+          end
+
+          it 'returns the UID of the user' do
+            expect(subject.uid).to eq '54'
+          end
+
+          context 'the info hash' do
+            subject { last_request.env['omniauth.auth']['info'] }
+
+            it 'includes user info attributes' do
+              expect(subject.name).to eq 'Peter Segel'
+              expect(subject.first_name).to eq 'Peter'
+              expect(subject.last_name).to eq 'Segel'
+              expect(subject.nickname).to eq 'psegel'
+              expect(subject.email).to eq 'psegel@intridea.com'
+              expect(subject.location).to eq 'Washington, D.C.'
+              expect(subject.image).to eq '/images/user.jpg'
+              expect(subject.phone).to eq '555-555-5555'
+            end
+          end
+
+          context 'the extra hash' do
+            subject { last_request.env['omniauth.auth']['extra'] }
+
+            it 'includes additional user attributes' do
+              expect(subject.user).to eq 'psegel'
+              expect(subject.employeeid).to eq '54'
+              expect(subject.hire_date).to eq '2004-07-13'
+            end
+          end
+
+          context 'the credentials hash' do
+            subject { last_request.env['omniauth.auth']['credentials'] }
+
+            it 'has a ticket value' do
+              expect(subject.ticket).to eq '593af'
+            end
+          end
+        end
+
+        it 'calls through to the master app' do
+          expect(last_response.body).to eq 'true'
+        end
+      end
+
+      let(:return_url) { 'http://127.0.0.10/?some=parameter' }
+
+      context 'with JASIG flavored XML' do
+        let(:xml_file_name) { 'cas_success_jasig.xml' }
+
+        it_behaves_like :successful_validation
+      end
+
+      context 'with classic XML' do
+        let(:xml_file_name) { 'cas_success.xml' }
+
+        it_behaves_like :successful_validation
+      end
+    end
+  end
+
+  describe 'POST /auth/cas3/callback' do
+    describe 'with a Single Sign-Out logoutRequest' do
+      let(:logoutRequest) do
+        %Q[
+          <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion\" ID="123abc-1234-ab12-cd34-1234abcd" Version="2.0" IssueInstant="#{Time.now.to_s}">
+            <saml:NameID>@NOT_USED@</saml:NameID>
+            <samlp:SessionIndex>ST-123456-123abc456def</samlp:SessionIndex>
+          </samlp:LogoutRequest>
+        ]
+      end
+
+      let(:logout_request) { double('logout_request', call:[200,{},'OK']) }
+
+      subject do
+        post 'auth/cas3/callback', logoutRequest:logoutRequest
+      end
+
+      before do
+        allow_any_instance_of(MyCasProvider)
+          .to receive(:logout_request_service)
+          .and_return double('LogoutRequest', new:logout_request)
+
+        subject
+      end
+
+      it 'initializes a LogoutRequest' do
+        expect(logout_request).to have_received :call
+      end
+    end
+  end
+end
diff --git a/vendor/gems/omniauth-cas3/spec/spec_helper.rb b/vendor/gems/omniauth-cas3/spec/spec_helper.rb
new file mode 100644
index 0000000000000000000000000000000000000000..75231268ff3aa3d56440f82734ff3609c4ad5c25
--- /dev/null
+++ b/vendor/gems/omniauth-cas3/spec/spec_helper.rb
@@ -0,0 +1,13 @@
+require 'bundler/setup'
+require 'awesome_print'
+
+RSpec.configure do |c|
+  c.filter_run focus: true
+  c.run_all_when_everything_filtered = true
+end
+
+require 'rack/test'
+require 'webmock/rspec'
+require 'omniauth-cas3'
+
+OmniAuth.config.logger = Logger.new( '/dev/null' )