├── .github
├── FUNDING.yml
└── workflows
│ └── ruby.yml
├── .gitignore
├── .project
├── .rspec
├── CHANGELOG.md
├── Gemfile
├── LICENSE
├── README.md
├── Rakefile
├── SECURITY.md
├── app
├── controllers
│ └── devise
│ │ └── cas_sessions_controller.rb
└── views
│ └── devise
│ └── cas_sessions
│ ├── new.html.erb
│ └── unregistered.html.erb
├── devise_cas_authenticatable.gemspec
├── lib
├── devise_cas_authenticatable.rb
└── devise_cas_authenticatable
│ ├── cas_action_url_factory_base.rb
│ ├── model.rb
│ ├── routes.rb
│ └── strategy.rb
├── rails
└── init.rb
└── spec
├── model_spec.rb
├── routes_spec.rb
├── scenario
├── .gitignore
├── app
│ ├── assets
│ │ └── config
│ │ │ └── manifest.js
│ ├── controllers
│ │ ├── application_controller.rb
│ │ └── home_controller.rb
│ ├── models
│ │ └── user.rb
│ └── views
│ │ └── layouts
│ │ └── application.html.erb
├── config.ru
├── config
│ ├── application.rb
│ ├── boot.rb
│ ├── database.yml
│ ├── environment.rb
│ ├── environments
│ │ ├── development.rb
│ │ ├── production.rb
│ │ └── test.rb
│ ├── initializers
│ │ ├── backtrace_silencers.rb
│ │ ├── devise.rb
│ │ ├── inflections.rb
│ │ ├── mime_types.rb
│ │ └── secret_token.rb
│ ├── locales
│ │ └── en.yml
│ ├── routes.rb
│ └── rubycas-server.yml
├── db
│ ├── migrate
│ │ ├── 20100401102949_create_tables.rb
│ │ ├── 20111002012903_add_sessions_table.rb
│ │ └── 20121009092400_add_deactivated_flag_to_users.rb
│ └── schema.rb
└── public
│ └── .gitkeep
├── spec_helper.rb
├── strategy_spec.rb
└── support
└── migrations.rb
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | tidelift: "rubygems/devise_cas_authenticatable"
4 |
--------------------------------------------------------------------------------
/.github/workflows/ruby.yml:
--------------------------------------------------------------------------------
1 | # This workflow uses actions that are not certified by GitHub.
2 | # They are provided by a third-party and are governed by
3 | # separate terms of service, privacy policy, and support
4 | # documentation.
5 | # This workflow will download a prebuilt Ruby version, install dependencies and run tests with Rake
6 | # For more information see: https://github.com/marketplace/actions/setup-ruby-jruby-and-truffleruby
7 |
8 | name: Ruby
9 |
10 | on:
11 | push:
12 | branches: [ main ]
13 | pull_request:
14 | branches: [ main ]
15 |
16 | jobs:
17 | test:
18 |
19 | runs-on: ubuntu-latest
20 | strategy:
21 | matrix:
22 | ruby-version: ['2.6', '2.7', '3.0']
23 |
24 | steps:
25 | - uses: actions/checkout@v2
26 | - name: Set up Ruby
27 | uses: ruby/setup-ruby@v1
28 | with:
29 | ruby-version: ${{ matrix.ruby-version }}
30 | bundler-cache: true # runs 'bundle install' and caches installed gems automatically
31 | - name: Run tests
32 | run: bundle exec rake spec
33 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .bundle/*
2 | .idea/*
3 | .yardoc/*
4 | pkg/*
5 | spec/scenario/db/*.sqlite3
6 | spec/scenario/tmp/*
7 | log/*
8 | db/*
9 | Gemfile.*lock
10 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
5 | Taking a lot of inspiration from [devise_ldap_authenticatable](http://github.com/cschiewek/devise_ldap_authenticatable)
6 |
7 | devise_cas_authenticatable is [CAS](http://www.jasig.org/cas) single sign-on support for
8 | [Devise](http://github.com/plataformatec/devise) applications. It acts as a replacement for
9 | database_authenticatable. It builds on [rack-cas](https://github.com/biola/rack-cas)
10 | and should support just about any conformant CAS server (although I have personally tested it
11 | using [rubycas-server](http://github.com/gunark/rubycas-server)).
12 |
13 | Requirements
14 | ------------
15 |
16 | - Rails 5.0 or greater
17 | - Devise 4.0 or greater
18 |
19 | devise_cas_authenticatable version 2 is a major rewrite
20 | -------------------------------------------------------
21 |
22 | devise_cas_authenticatable version 1 was based on
23 | [rubycas-client](https://github.com/rubycas/rubycas-client). Now that rubycas-client is deprecated,
24 | devise_cas_authenticatable version 2 is based on [rack-cas](https://github.com/biola/rack-cas).
25 |
26 | In order to upgrade, you'll need to:
27 |
28 | * Make sure you're on a supported version of Devise (4.0 or above) and a supported version of Rails
29 | (5.0 or above)
30 | * Add the rack-cas configuration to your application.rb (see below)
31 | * Remove the cas_base_url, cas_login_url, cas_logout_url, cas_validate_url, and
32 | cas_client_config_options from your devise.rb initializer, if present
33 | * If using single sign out: [set up rack-cas's built-in single sign out support](https://github.com/biola/rack-cas#single-logout)
34 |
35 | Installation
36 | ------------
37 |
38 | Add to your Gemfile:
39 |
40 | gem 'devise'
41 | gem 'devise_cas_authenticatable'
42 |
43 | Setup
44 | -----
45 |
46 | Once devise\_cas\_authenticatable is installed, add the following to your user model:
47 |
48 | ```ruby
49 | devise :cas_authenticatable
50 | ```
51 |
52 | You can also add other modules such as token_authenticatable, trackable, etc. Please do not
53 | add database_authenticatable as this module is intended to replace it.
54 |
55 | You'll also need to set up the database schema for this:
56 |
57 | ```ruby
58 | create_table :users do |t|
59 | t.string :username, :null => false
60 | end
61 | ```
62 |
63 | We also recommend putting a unique index on the `username` column:
64 |
65 | ```ruby
66 | add_index :users, :username, :unique => true
67 | ```
68 |
69 | (Note: previously, devise\_cas\_authenticatable recommended using a `t.cas_authenticatable` method call to update the
70 | schema. Devise 2.0 has deprecated this type of schema building method, so we now recommend just adding the `username`
71 | string column as above. As of this writing, `t.cas_authenticatable` still works, but throws a deprecation warning in
72 | Devise 2.0.)
73 |
74 | You'll need to configure rack-cas so that it knows where your CAS server is. See the
75 | [rack-cas README](https://github.com/biola/rack-cas) for full instructions, but here is the
76 | bare minimum:
77 |
78 | ```ruby
79 | config.rack_cas.server_url = "https://cas.myorganization.com" # replace with your server URL
80 | config.rack_cas.service = "/users/service" # If your user model isn't called User, change this
81 | ```
82 |
83 | Finally, you may need to add some configuration to your config/initializers/devise.rb in order
84 | to tell your app how to talk to your CAS server. This isn't always required. Here's an example:
85 |
86 | ```ruby
87 | Devise.setup do |config|
88 | ...
89 | # The CAS specification allows for the passing of a follow URL to be displayed when
90 | # a user logs out on the CAS server. RubyCAS-Server also supports redirecting to a
91 | # URL via the destination param. Set either of these urls and specify either nil,
92 | # 'destination' or 'follow' as the logout_url_param. If the urls are blank but
93 | # logout_url_param is set, a default will be detected for the service.
94 | # config.cas_destination_url = 'https://cas.myorganization.com'
95 | # config.cas_follow_url = 'https://cas.myorganization.com'
96 | # config.cas_logout_url_param = nil
97 |
98 | # You can specify the name of the destination argument with the following option.
99 | # e.g. the following option will change it from 'destination' to 'url'
100 | # config.cas_destination_logout_param_name = 'url'
101 |
102 | # By default, devise_cas_authenticatable will create users. If you would rather
103 | # require user records to already exist locally before they can authenticate via
104 | # CAS, uncomment the following line.
105 | # config.cas_create_user = false
106 |
107 | # If you don't want to use the username returned from your CAS server as the unique
108 | # identifier, but some other field passed in cas_extra_attributes, you can specify
109 | # the field name here.
110 | # config.cas_user_identifier = nil
111 | end
112 | ```
113 |
114 | Extra attributes
115 | ----------------
116 |
117 | If your CAS server passes along extra attributes you'd like to save in your user records,
118 | using the CAS extra_attributes parameter, you can define a method in your user model called
119 | cas_extra_attributes= to accept these. For example:
120 |
121 | ```ruby
122 | class User < ActiveRecord::Base
123 | devise :cas_authenticatable
124 |
125 | def cas_extra_attributes=(extra_attributes)
126 | extra_attributes.each do |name, value|
127 | case name.to_sym
128 | when :fullname
129 | self.fullname = value
130 | when :email
131 | self.email = value
132 | end
133 | end
134 | end
135 | end
136 | ```
137 |
138 | Using without a database
139 | ------------------------
140 |
141 | You don't have to save your user model to the database - you can simply store it in the session as is.
142 | You can follow the following approach (inspired by [this article](https://4trabes.com/2012/10/31/remote-authentication-with-devise/)):
143 |
144 | ```ruby
145 | require 'active_model'
146 |
147 | class User
148 | attr_accessor :id, :extra_attributes
149 |
150 | include ActiveModel::Validations
151 | extend ActiveModel::Callbacks
152 | extend Devise::Models
153 | define_model_callbacks :validation
154 |
155 | class << self
156 | # override these methods to work nicely with Devise
157 | def serialize_from_session(id, _)
158 | return nil if id.nil?
159 | self.new(id: id)
160 | end
161 |
162 | def serialize_into_session(record)
163 | [record.id, '']
164 | end
165 |
166 | def logger
167 | ActiveRecord::Base.logger # e.g. assuming you are using Rails
168 | end
169 |
170 | # Overload of default callback to ensure we don't try to create any database records.
171 | def authenticate_with_cas_details(cas_details)
172 | self.new(cas_details['extra_attributes'])
173 | end
174 | end
175 |
176 | def initialize(extra_attributes: nil, id: nil)
177 | self.extra_attributes = extra_attributes
178 | self.id = id
179 | end
180 |
181 | devise :cas_authenticatable
182 | end
183 | ```
184 |
185 | See also
186 | --------
187 |
188 | * [CAS](http://www.jasig.org/cas)
189 | * [rack-cas](https://github.com/biola/rack-cas)
190 | * [Devise](http://github.com/plataformatec/devise)
191 | * [Warden](http://github.com/hassox/warden)
192 |
193 | License
194 | -------
195 |
196 | `devise_cas_authenticatable` is released under the terms and conditions of the MIT license. See the LICENSE file for more
197 | information.
198 |
--------------------------------------------------------------------------------
/Rakefile:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env rake
2 | require 'bundler/gem_tasks'
3 |
4 | Bundler.setup
5 |
6 | require 'rspec/mocks/version'
7 | require 'rspec/core/rake_task'
8 |
9 | RSpec::Core::RakeTask.new(:spec)
10 |
11 | require File.expand_path('../spec/scenario/config/application', __FILE__)
12 |
13 | Scenario::Application.load_tasks
14 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Supported Versions
4 |
5 | The most recent major version of devise_cas_authenticatable is supported with security updates. For example, if the current version of the package is 1.3.2,
6 | and we receive a vulnerability report and fix it, we will release version 1.3.3. Because we use semantic versioning, it should always be safe for users
7 | of any 1.x version to update to this release.
8 |
9 | ## Reporting a Vulnerability
10 |
11 | We use Tidelift for coordinated vulnerability disclosure. To report vulnerabilities, go to https://tidelift.com/security.
12 |
--------------------------------------------------------------------------------
/app/controllers/devise/cas_sessions_controller.rb:
--------------------------------------------------------------------------------
1 | class Devise::CasSessionsController < Devise::SessionsController
2 | def new
3 | # TODO: Figure out if there's a less hacky way to do this
4 | RackCAS.config.service = cas_service_url
5 | head 401
6 | end
7 |
8 | def service
9 | redirect_to after_sign_in_path_for(warden.authenticate!(:scope => resource_name)), allow_other_host: true
10 | end
11 |
12 | def unregistered; end
13 |
14 | def destroy
15 | # if :cas_create_user is false a CAS session might be open but not signed_in
16 | # in such case we destroy the session here
17 | if signed_in?(resource_name)
18 | sign_out(resource_name)
19 | session.delete('cas')
20 | else
21 | reset_session
22 | end
23 |
24 | redirect_to(cas_logout_url, allow_other_host: true)
25 | end
26 |
27 | private
28 |
29 | def cas_login_url
30 | RackCAS::Server.new(RackCAS.config.server_url).login_url(cas_service_url).to_s
31 | end
32 | helper_method :cas_login_url
33 |
34 | def request_url
35 | return @request_url if @request_url
36 |
37 | @request_url = request.protocol.dup
38 | @request_url << request.host
39 | @request_url << ":#{request.port}" unless request.port == 80
40 | @request_url
41 | end
42 |
43 | def cas_destination_url
44 | return unless ::Devise.cas_logout_url_param == 'destination'
45 |
46 | if !::Devise.cas_destination_url.blank?
47 | Devise.cas_destination_url
48 | else
49 | url = request_url.dup
50 | url << after_sign_out_path_for(resource_name)
51 | end
52 | end
53 |
54 | def cas_follow_url
55 | return unless ::Devise.cas_logout_url_param == 'follow'
56 |
57 | if !::Devise.cas_follow_url.blank?
58 | Devise.cas_follow_url
59 | else
60 | url = request_url.dup
61 | url << after_sign_out_path_for(resource_name)
62 | end
63 | end
64 |
65 | def cas_service_url
66 | ::Devise.cas_service_url(request_url.dup, devise_mapping)
67 | end
68 |
69 | def cas_logout_url
70 | server = RackCAS::Server.new(RackCAS.config.server_url)
71 | destination_url = cas_destination_url
72 | follow_url = cas_follow_url
73 | service_url = cas_service_url
74 |
75 | if destination_url
76 | server.logout_url(destination: destination_url, gateway: 'true').to_s
77 | elsif follow_url
78 | server.logout_url(url: follow_url, service: service_url).to_s
79 | else
80 | server.logout_url(service: service_url).to_s
81 | end
82 | end
83 | end
84 |
--------------------------------------------------------------------------------
/app/views/devise/cas_sessions/new.html.erb:
--------------------------------------------------------------------------------
1 |
Click <%= link_to "here", cas_login_url %> to sign in.
-------------------------------------------------------------------------------- /app/views/devise/cas_sessions/unregistered.html.erb: -------------------------------------------------------------------------------- 1 |The user <%=h params[:username] %> is not registered with this site. 2 | Please <%= link_to "sign in using a different account", 3 | RackCAS::Server.new(RackCAS.config.server_url).logout_url(destination: send("new_#{resource_name}_session_url")).to_s %>.
4 | -------------------------------------------------------------------------------- /devise_cas_authenticatable.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'devise_cas_authenticatable' 3 | s.version = '2.0.2' 4 | 5 | s.required_rubygems_version = Gem::Requirement.new('> 1.3.1') if s.respond_to? :required_rubygems_version= 6 | s.authors = ['Nat Budin', 'Jeremy Haile'] 7 | s.description = 'CAS authentication module for Devise' 8 | s.license = 'MIT' 9 | s.email = 'natbudin@gmail.com' 10 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 11 | s.files = `git ls-files`.split("\n") 12 | s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") 13 | s.extra_rdoc_files = [ 14 | 'README.md' 15 | ] 16 | 17 | s.homepage = 'http://github.com/nbudin/devise_cas_authenticatable' 18 | s.require_paths = ['lib'] 19 | s.rubygems_version = '1.5.0' 20 | s.summary = 'CAS authentication module for Devise' 21 | 22 | s.add_runtime_dependency('devise', ['>= 4.0.0']) 23 | s.add_runtime_dependency('rack-cas') 24 | 25 | s.add_development_dependency('capybara') 26 | s.add_development_dependency('database_cleaner-active_record') 27 | s.add_development_dependency('launchy') 28 | s.add_development_dependency('pry') 29 | s.add_development_dependency('rails') 30 | s.add_development_dependency('rspec-rails') 31 | s.add_development_dependency('sqlite3') 32 | end 33 | -------------------------------------------------------------------------------- /lib/devise_cas_authenticatable.rb: -------------------------------------------------------------------------------- 1 | require 'devise' 2 | require 'rack-cas' 3 | require 'rack-cas/server' 4 | 5 | require 'devise_cas_authenticatable/routes' 6 | require 'devise_cas_authenticatable/strategy' 7 | require 'devise_cas_authenticatable/cas_action_url_factory_base' 8 | 9 | module DeviseCasAuthenticatable 10 | class Engine < Rails::Engine ; end 11 | end 12 | 13 | module Devise 14 | # The destination url for logout. 15 | @@cas_destination_url = nil 16 | 17 | # The follow url for logout. 18 | @@cas_follow_url = nil 19 | 20 | # Which url to send with logout, destination or follow. Can either be nil, destination or follow. 21 | @@cas_logout_url_param = nil 22 | 23 | # Should devise_cas_authenticatable attempt to create new user records for 24 | # unknown usernames? True by default. 25 | @@cas_create_user = true 26 | 27 | # The model attribute used for query conditions. :username by default 28 | @@cas_username_column = :username 29 | 30 | # The CAS reponse value used to find users in the local database 31 | # it is required that this field be in cas_extra_attributes 32 | @@cas_user_identifier = nil 33 | 34 | # Name of the parameter passed in the logout query 35 | @@cas_destination_logout_param_name = nil 36 | 37 | mattr_accessor :cas_destination_url, :cas_follow_url, :cas_logout_url_param, :cas_create_user, :cas_destination_logout_param_name, :cas_username_column, :cas_user_identifier 38 | 39 | def self.cas_create_user? 40 | cas_create_user 41 | end 42 | 43 | def self.cas_service_url(base_url, mapping) 44 | cas_action_url(base_url, mapping, 'service') 45 | end 46 | 47 | def self.cas_unregistered_url(base_url, mapping) 48 | cas_action_url(base_url, mapping, 'unregistered') 49 | end 50 | 51 | def self.cas_action_url(base_url, mapping, action) 52 | cas_action_url_factory_class.new(base_url, mapping, action).call 53 | end 54 | 55 | def self.cas_action_url_factory_class 56 | @cas_action_url_factory_class ||= CasActionUrlFactoryBase.prepare_class 57 | end 58 | 59 | def self.cas_enable_single_sign_out=(_value) 60 | puts "Devise.cas_enable_single_sign_out is deprecated as of devise_cas_authenticatable 2.0, and has no effect." 61 | puts "Single sign out is now handled via rack-cas. To set it up, see the rack-cas readme:" 62 | puts "https://github.com/biola/rack-cas#single-logout" 63 | end 64 | 65 | def self.cas_single_sign_out_mapping_strategy=(_value) 66 | puts "Devise.cas_single_sign_out_mapping_strategy is deprecated as of devise_cas_authenticatable 2.0, and has no effect." 67 | puts "Single sign out is now handled via rack-cas. To set it up, see the rack-cas readme:" 68 | puts "https://github.com/biola/rack-cas#single-logout" 69 | end 70 | end 71 | 72 | Devise.add_module( 73 | :cas_authenticatable, 74 | strategy: true, 75 | controller: :cas_sessions, 76 | route: :cas_authenticatable, 77 | model: 'devise_cas_authenticatable/model' 78 | ) 79 | -------------------------------------------------------------------------------- /lib/devise_cas_authenticatable/cas_action_url_factory_base.rb: -------------------------------------------------------------------------------- 1 | module Devise 2 | class CasActionUrlFactoryBase 3 | attr_reader :base_url, :mapping, :action 4 | 5 | def self.prepare_class 6 | Class.new(self) do 7 | include Rails.application.routes.url_helpers 8 | 9 | if Rails.application.routes.respond_to?(:mounted_helpers) && Rails.application.routes.mounted_helpers 10 | include Rails.application.routes.mounted_helpers 11 | end 12 | end 13 | end 14 | 15 | def initialize(base_url, mapping, action) 16 | @base_url = base_url 17 | @mapping = mapping 18 | @action = action 19 | end 20 | 21 | def call 22 | uri = URI.parse(base_url).tap { |uri| uri.query = nil } 23 | uri.path = load_base_path 24 | uri.to_s 25 | end 26 | 27 | alias_method :build, :call 28 | 29 | private 30 | def load_base_path 31 | load_routes_path || load_mapping_path 32 | end 33 | 34 | def load_routes_path 35 | router_name = mapping.router_name || Devise.available_router_name 36 | context = send(router_name) 37 | 38 | route = "#{mapping.singular}_#{action}_path" 39 | if context.respond_to? route 40 | context.send route 41 | else 42 | nil 43 | end 44 | rescue NameError, NoMethodError 45 | nil 46 | end 47 | 48 | def load_mapping_path 49 | path = mapping_fullpath || mapping_raw_path 50 | path << "/" unless path =~ /\/$/ 51 | path << action 52 | path 53 | end 54 | 55 | def mapping_fullpath 56 | return nil unless mapping.respond_to?(:fullpath) 57 | "#{rails_relative_url_root}#{mapping.fullpath}" 58 | end 59 | 60 | def mapping_raw_path 61 | "#{rails_relative_url_root}#{mapping.raw_path}" 62 | end 63 | 64 | def rails_relative_url_root 65 | ENV['RAILS_RELATIVE_URL_ROOT'] 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/devise_cas_authenticatable/model.rb: -------------------------------------------------------------------------------- 1 | module Devise 2 | module Models 3 | # Extends your User class with support for CAS ticket authentication. 4 | module CasAuthenticatable 5 | def self.included(base) 6 | base.extend ClassMethods 7 | end 8 | 9 | module ClassMethods 10 | # Given a CAS details hash returned by rack-cas, return the resulting user object. 11 | # Behavior is as follows: 12 | # 13 | # * Find a matching user by username (will use find_for_authentication if available). 14 | # * If the user does not exist, but Devise.cas_create_user is set, attempt to create the 15 | # user object in the database. If cas_extra_attributes= is defined, this will also 16 | # pass in the extra_attributes hash. 17 | # * Return the resulting user object. 18 | def authenticate_with_cas_details(cas_details) 19 | identifier = cas_details['user'] 20 | 21 | # If cas_user_identifier isn't in extra_attributes, 22 | # or the value is blank, then we're done here 23 | return log_and_exit if identifier.nil? 24 | 25 | logger.debug("Using conditions {#{::Devise.cas_username_column} => #{identifier}} to find the User") 26 | 27 | conditions = { ::Devise.cas_username_column => identifier } 28 | resource = find_or_build_resource_from_conditions(conditions) 29 | return nil unless resource 30 | 31 | if resource.respond_to?(:cas_extra_attributes=) 32 | resource.cas_extra_attributes = cas_details['extra_attributes'] 33 | end 34 | 35 | resource.save 36 | resource 37 | end 38 | 39 | private 40 | 41 | def should_create_cas_users? 42 | respond_to?(:cas_create_user?) ? cas_create_user? : ::Devise.cas_create_user? 43 | end 44 | 45 | def extract_user_identifier(response) 46 | return response.user if ::Devise.cas_user_identifier.blank? 47 | response.extra_attributes[::Devise.cas_user_identifier] 48 | end 49 | 50 | def log_and_exit 51 | logger.warn("Could not find a value for [#{::Devise.cas_user_identifier}] in cas_extra_attributes so we cannot find the User.") 52 | logger.warn("Make sure config.cas_user_identifier is set to a field that appears in cas_extra_attributes") 53 | return nil 54 | end 55 | 56 | def find_or_build_resource_from_conditions(conditions) 57 | resource = find_resource_with_conditions(conditions) 58 | resource = new(conditions) if (resource.nil? and should_create_cas_users?) 59 | return resource 60 | end 61 | 62 | def find_resource_with_conditions(conditions) 63 | find_for_authentication(conditions) 64 | end 65 | end 66 | end 67 | end 68 | end 69 | -------------------------------------------------------------------------------- /lib/devise_cas_authenticatable/routes.rb: -------------------------------------------------------------------------------- 1 | ActionDispatch::Routing::Mapper.class_eval do 2 | protected 3 | 4 | def devise_cas_authenticatable(mapping, controllers) 5 | sign_out_via = (Devise.respond_to?(:sign_out_via) && Devise.sign_out_via) || [:get, :post] 6 | 7 | # service endpoint for CAS server 8 | get 'service', to: "#{controllers[:cas_sessions]}#service", as: 'service' 9 | 10 | resource :session, only: [], controller: controllers[:cas_sessions], path: '' do 11 | get :new, path: mapping.path_names[:sign_in], as: 'new' 12 | get :unregistered 13 | post :create, path: mapping.path_names[:sign_in] 14 | match :destroy, path: mapping.path_names[:sign_out], as: 'destroy', via: sign_out_via 15 | end 16 | end 17 | 18 | def raise_no_secret_key #:nodoc: 19 | # Devise_cas_authenticatable does not store passwords, so does not need a secret! 20 | Rails.logger.warn <<~WARNING 21 | Devise_cas_authenticatable has suppressed an exception from being raised for missing Devise.secret_key. 22 | If devise_cas_authenticatable is the only devise module you are using for authentication you can safely ignore this warning. 23 | However, if you use another module that requires the secret_key please follow these instructions from Devise: 24 | 25 | Devise.secret_key was not set. Please add the following to your Devise initializer: 26 | 27 | config.secret_key = '#{SecureRandom.hex(64)}' 28 | 29 | Please ensure you restarted your application after installing Devise or setting the key. 30 | WARNING 31 | end 32 | end 33 | 34 | -------------------------------------------------------------------------------- /lib/devise_cas_authenticatable/strategy.rb: -------------------------------------------------------------------------------- 1 | require 'devise/strategies/base' 2 | 3 | module Devise 4 | module Strategies 5 | class CasAuthenticatable < Base 6 | # True if the mapping supports authenticate_with_cas_ticket. 7 | def valid? 8 | request = Rack::Request.new(env) 9 | mapping.to.respond_to?(:authenticate_with_cas_details) && request.session['cas'] 10 | end 11 | 12 | # Try to authenticate a user using the CAS ticket passed in params. 13 | # If the ticket is valid and the model's authenticate_with_cas_ticket method 14 | # returns a user, then return success. If the ticket is invalid, then either 15 | # fail (if we're just returning from the CAS server, based on the referrer) 16 | # or attempt to redirect to the CAS server's login URL. 17 | def authenticate! 18 | request = Rack::Request.new(env) 19 | cas_details = request.session['cas'] 20 | if cas_details 21 | resource = mapping.to.authenticate_with_cas_details(cas_details) 22 | if resource 23 | success!(resource) 24 | else 25 | username = cas_details['user'] 26 | redirect!(::Devise.cas_unregistered_url(request.url, mapping), :username => username) 27 | end 28 | else 29 | # Throw to rack-cas to initiate a login 30 | rack_cas_authenticate_response = Rack::Response.new(nil, 401) 31 | custom!(rack_cas_authenticate_response.to_a) 32 | throw :warden 33 | end 34 | end 35 | end 36 | end 37 | end 38 | 39 | Warden::Strategies.add(:cas_authenticatable, Devise::Strategies::CasAuthenticatable) 40 | -------------------------------------------------------------------------------- /rails/init.rb: -------------------------------------------------------------------------------- 1 | require "devise_cas_authenticatable" -------------------------------------------------------------------------------- /spec/model_spec.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Metrics/BlockLength 2 | 3 | require 'spec_helper' 4 | 5 | describe Devise::Models::CasAuthenticatable do 6 | 7 | describe 'When the user lookup is by something other than username' do 8 | before(:each) do 9 | Devise.cas_create_user = false 10 | end 11 | 12 | it 'should authenticate using whatever is specified in config.cas_user_identifier' do 13 | Devise.cas_user_identifier = :id 14 | Devise.cas_username_column = :id 15 | 16 | user = User.create!(username: 'testusername') 17 | User.authenticate_with_cas_details(cas_details_for_user(user)) 18 | 19 | # Reset this otherwise it'll blow up other specs 20 | Devise.cas_user_identifier = nil 21 | end 22 | 23 | it 'should authenticate as normal is config.cas_user_identifier is not set' do 24 | Devise.cas_user_identifier = nil 25 | Devise.cas_username_column = :username 26 | 27 | user = User.create!(username: 'testusername') 28 | User.authenticate_with_cas_details(cas_details_for_user(user)) 29 | end 30 | 31 | it 'should return nil if cas_user_identifier is not in cas_extra_attributes' do 32 | Devise.cas_user_identifier = :unknown_ticket_field 33 | Devise.cas_username_column = :username 34 | expect( 35 | User.authenticate_with_cas_details( 36 | { 37 | 'user' => 'testusername', 38 | 'extra_attributes' => { id: 10 } 39 | } 40 | ) 41 | ).to be_nil 42 | 43 | # Reset this otherwise it'll blow up other specs 44 | Devise.cas_user_identifier = nil 45 | end 46 | 47 | def cas_details_for_user(user) 48 | { 'user' => user.username, 'extra_attributes' => { id: user.id } } 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /spec/routes_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe 'routing' do 4 | include RSpec::Rails::RoutingExampleGroup 5 | 6 | it 'routes to #service' do 7 | expect(get('/users/service')).to route_to('devise/cas_sessions#service') 8 | end 9 | 10 | it 'routes to #new' do 11 | expect(get('/users/sign_in')).to route_to('devise/cas_sessions#new') 12 | end 13 | 14 | it 'routes to #create' do 15 | expect(post('/users/sign_in')).to route_to('devise/cas_sessions#create') 16 | end 17 | 18 | it 'routes to #destroy' do 19 | expect(delete('/users/sign_out')).to route_to('devise/cas_sessions#destroy') 20 | end 21 | 22 | it 'routes to #unregistered' do 23 | expect(get('/users/unregistered')).to route_to('devise/cas_sessions#unregistered') 24 | end 25 | end 26 | 27 | describe Devise::CasSessionsController do 28 | include RSpec::Rails::ControllerExampleGroup 29 | 30 | it 'should have the right route names' do 31 | expect(controller).to respond_to('user_service_path', 'new_user_session_path', 'user_session_path', 'destroy_user_session_path') 32 | expect(controller.user_service_path).to eq('/users/service') 33 | expect(controller.new_user_session_path).to eq('/users/sign_in') 34 | expect(controller.user_session_path).to eq('/users/sign_in') 35 | expect(controller.destroy_user_session_path).to eq('/users/sign_out') 36 | expect(controller.unregistered_user_session_path).to eq('/users/unregistered') 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /spec/scenario/.gitignore: -------------------------------------------------------------------------------- 1 | .bundle 2 | db/*.sqlite3 3 | log/*.log 4 | tmp/**/* 5 | -------------------------------------------------------------------------------- /spec/scenario/app/assets/config/manifest.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbudin/devise_cas_authenticatable/54ab4e8c282ac26f0bf659da400d29945fa9cda1/spec/scenario/app/assets/config/manifest.js -------------------------------------------------------------------------------- /spec/scenario/app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | protect_from_forgery 3 | end 4 | -------------------------------------------------------------------------------- /spec/scenario/app/controllers/home_controller.rb: -------------------------------------------------------------------------------- 1 | class HomeController < ApplicationController 2 | before_action :authenticate_user! 3 | 4 | def index 5 | head(:ok) 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/scenario/app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | devise :cas_authenticatable, :rememberable, :timeoutable, :trackable 3 | 4 | def active_for_authentication? 5 | super && !deactivated 6 | end 7 | end -------------------------------------------------------------------------------- /spec/scenario/app/views/layouts/application.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |<%= alert %>
10 |<%= notice %>
11 | 12 | <%= yield %> 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /spec/scenario/config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Scenario::Application 5 | -------------------------------------------------------------------------------- /spec/scenario/config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require 'rails/all' 4 | 5 | Bundler.require(:default, Rails.env) if defined?(Bundler) 6 | 7 | module Scenario 8 | class Application < Rails::Application 9 | config.active_support.deprecation = :stderr 10 | config.rack_cas.fake = true 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/scenario/config/boot.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | 3 | # Set up gems listed in the Gemfile. 4 | gemfile = File.expand_path('../../Gemfile', __FILE__) 5 | begin 6 | ENV['BUNDLE_GEMFILE'] = gemfile 7 | require 'bundler' 8 | Bundler.setup 9 | rescue Bundler::GemNotFound => e 10 | STDERR.puts e.message 11 | STDERR.puts "Try running `bundle install`." 12 | exit! 13 | end if File.exist?(gemfile) 14 | -------------------------------------------------------------------------------- /spec/scenario/config/database.yml: -------------------------------------------------------------------------------- 1 | # SQLite version 3.x 2 | # gem install sqlite3-ruby (not necessary on OS X Leopard) 3 | development: 4 | adapter: sqlite3 5 | database: db/development.sqlite3 6 | pool: 5 7 | timeout: 5000 8 | 9 | # Warning: The database defined as "test" will be erased and 10 | # re-generated from your development database when you run "rake". 11 | # Do not set this db to the same as development or production. 12 | test: 13 | adapter: sqlite3 14 | database: db/test.sqlite3 15 | pool: 5 16 | timeout: 5000 17 | 18 | production: 19 | adapter: sqlite3 20 | database: db/production.sqlite3 21 | pool: 5 22 | timeout: 5000 23 | -------------------------------------------------------------------------------- /spec/scenario/config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the rails application 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the rails application 5 | Scenario::Application.initialize! 6 | -------------------------------------------------------------------------------- /spec/scenario/config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Scenario::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the webserver when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Log error messages when you accidentally call methods on nil. 10 | config.whiny_nils = true 11 | 12 | # Show full error reports and disable caching 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Don't care if the mailer can't send 17 | config.action_mailer.raise_delivery_errors = false 18 | 19 | # Print deprecation notices to the Rails logger 20 | config.active_support.deprecation = :log 21 | 22 | # Only use best-standards-support built into browsers 23 | config.action_dispatch.best_standards_support = :builtin 24 | end 25 | 26 | -------------------------------------------------------------------------------- /spec/scenario/config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Scenario::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | 4 | # The production environment is meant for finished, "live" apps. 5 | # Code is not reloaded between requests 6 | config.cache_classes = true 7 | 8 | # Full error reports are disabled and caching is turned on 9 | config.consider_all_requests_local = false 10 | config.action_controller.perform_caching = true 11 | 12 | # Specifies the header that your server uses for sending files 13 | config.action_dispatch.x_sendfile_header = "X-Sendfile" 14 | 15 | # For nginx: 16 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' 17 | 18 | # If you have no front-end server that supports something like X-Sendfile, 19 | # just comment this out and Rails will serve the files 20 | 21 | # See everything in the log (default is :info) 22 | # config.log_level = :debug 23 | 24 | # Use a different logger for distributed setups 25 | # config.logger = SyslogLogger.new 26 | 27 | # Use a different cache store in production 28 | # config.cache_store = :mem_cache_store 29 | 30 | # Disable Rails's static asset server 31 | # In production, Apache or nginx will already do this 32 | config.serve_static_assets = false 33 | 34 | # Enable serving of images, stylesheets, and javascripts from an asset server 35 | # config.action_controller.asset_host = "http://assets.example.com" 36 | 37 | # Disable delivery errors, bad email addresses will be ignored 38 | # config.action_mailer.raise_delivery_errors = false 39 | 40 | # Enable threaded mode 41 | # config.threadsafe! 42 | 43 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 44 | # the I18n.default_locale when a translation can not be found) 45 | config.i18n.fallbacks = true 46 | 47 | # Send deprecation notices to registered listeners 48 | config.active_support.deprecation = :notify 49 | end 50 | -------------------------------------------------------------------------------- /spec/scenario/config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Scenario::Application.configure do 2 | # Settings specified here will take precedence over those in config/environment.rb 3 | config.eager_load = false 4 | 5 | # The test environment is used exclusively to run your application's 6 | # test suite. You never need to work with it otherwise. Remember that 7 | # your test database is "scratch space" for the test suite and is wiped 8 | # and recreated between test runs. Don't rely on the data there! 9 | config.cache_classes = true 10 | 11 | # Log error messages when you accidentally call methods on nil. 12 | config.whiny_nils = true 13 | 14 | # Show full error reports and disable caching 15 | config.consider_all_requests_local = true 16 | config.action_controller.perform_caching = false 17 | 18 | # Raise exceptions instead of rendering exception templates 19 | config.action_dispatch.show_exceptions = false 20 | 21 | # Disable request forgery protection in test environment 22 | config.action_controller.allow_forgery_protection = false 23 | 24 | # Tell Action Mailer not to deliver emails to the real world. 25 | # The :test delivery method accumulates sent emails in the 26 | # ActionMailer::Base.deliveries array. 27 | config.action_mailer.delivery_method = :test 28 | 29 | # Use SQL instead of Active Record's schema dumper when creating the test database. 30 | # This is necessary if your schema can't be completely dumped by the schema dumper, 31 | # like if you have constraints or database-specific column types 32 | # config.active_record.schema_format = :sql 33 | 34 | # Print deprecation notices to the stderr 35 | config.active_support.deprecation = :stderr 36 | end 37 | -------------------------------------------------------------------------------- /spec/scenario/config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /spec/scenario/config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | Devise.setup do |config| 2 | require "devise/orm/active_record" 3 | config.timeout_in = 7200.seconds 4 | end 5 | -------------------------------------------------------------------------------- /spec/scenario/config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format 4 | # (all these examples are active by default): 5 | # ActiveSupport::Inflector.inflections do |inflect| 6 | # inflect.plural /^(ox)$/i, '\1en' 7 | # inflect.singular /^(ox)en/i, '\1' 8 | # inflect.irregular 'person', 'people' 9 | # inflect.uncountable %w( fish sheep ) 10 | # end 11 | -------------------------------------------------------------------------------- /spec/scenario/config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | # Mime::Type.register_alias "text/html", :iphone 6 | -------------------------------------------------------------------------------- /spec/scenario/config/initializers/secret_token.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | # Make sure the secret is at least 30 characters and all random, 6 | # no regular words or you'll be exposed to dictionary attacks. 7 | Scenario::Application.config.secret_token = '70d2ec936ec5a91e883a9dc74bfeadd5a96cc242d3fd0857aa0151112ac71721475e01ae788e5c976a09ab62dd20240678cdc393c37cb777e872e59ea74adaad' 8 | -------------------------------------------------------------------------------- /spec/scenario/config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Sample localization file for English. Add more files in this directory for other locales. 2 | # See http://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. 3 | 4 | en: 5 | hello: "Hello world" 6 | -------------------------------------------------------------------------------- /spec/scenario/config/routes.rb: -------------------------------------------------------------------------------- 1 | Scenario::Application.routes.draw do 2 | devise_for :users 3 | root :to => "home#index" 4 | end 5 | -------------------------------------------------------------------------------- /spec/scenario/config/rubycas-server.yml: -------------------------------------------------------------------------------- 1 | url_path: /cas_server 2 | 3 | log: 4 | level: DEBUG 5 | 6 | database: 7 | adapter: sqlite3 8 | database: db/cas.sqlite3 9 | pool: 5 10 | timeout: 5000 11 | 12 | authenticator: 13 | class: TestAuthenticator -------------------------------------------------------------------------------- /spec/scenario/db/migrate/20100401102949_create_tables.rb: -------------------------------------------------------------------------------- 1 | class CreateTables < ActiveRecord::Migration[4.2] 2 | def self.up 3 | create_table :users do |t| 4 | t.string :username, :null => false 5 | t.datetime :remember_created_at 6 | t.string :email 7 | 8 | # trackable 9 | t.integer :sign_in_count 10 | t.datetime :current_sign_in_at 11 | t.datetime :last_sign_in_at 12 | t.string :current_sign_in_ip 13 | t.string :last_sign_in_ip 14 | 15 | t.timestamps 16 | end 17 | 18 | add_index :users, :username, :unique => true 19 | end 20 | 21 | def self.down 22 | drop_table :users 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /spec/scenario/db/migrate/20111002012903_add_sessions_table.rb: -------------------------------------------------------------------------------- 1 | class AddSessionsTable < ActiveRecord::Migration[4.2] 2 | def self.up 3 | create_table :sessions do |t| 4 | t.string :session_id, :null => false 5 | t.text :data 6 | t.timestamps 7 | end 8 | 9 | add_index :sessions, :session_id 10 | add_index :sessions, :updated_at 11 | end 12 | 13 | def self.down 14 | drop_table :sessions 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /spec/scenario/db/migrate/20121009092400_add_deactivated_flag_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddDeactivatedFlagToUsers < ActiveRecord::Migration[4.2] 2 | def self.up 3 | add_column :users, :deactivated, :boolean 4 | end 5 | 6 | def self.down 7 | remove_column :users, :deactivated 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/scenario/db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended to check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(:version => 20121009092400) do 15 | 16 | create_table "sessions", :force => true do |t| 17 | t.string "session_id", :null => false 18 | t.text "data" 19 | t.datetime "created_at", :null => false 20 | t.datetime "updated_at", :null => false 21 | end 22 | 23 | add_index "sessions", ["session_id"], :name => "index_sessions_on_session_id" 24 | add_index "sessions", ["updated_at"], :name => "index_sessions_on_updated_at" 25 | 26 | create_table "users", :force => true do |t| 27 | t.string "username", :null => false 28 | t.datetime "remember_created_at" 29 | t.string "email" 30 | t.integer "sign_in_count" 31 | t.datetime "current_sign_in_at" 32 | t.datetime "last_sign_in_at" 33 | t.string "current_sign_in_ip" 34 | t.string "last_sign_in_ip" 35 | t.datetime "created_at", :null => false 36 | t.datetime "updated_at", :null => false 37 | t.boolean "deactivated" 38 | end 39 | 40 | add_index "users", ["username"], :name => "index_users_on_username", :unique => true 41 | 42 | end 43 | -------------------------------------------------------------------------------- /spec/scenario/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nbudin/devise_cas_authenticatable/54ab4e8c282ac26f0bf659da400d29945fa9cda1/spec/scenario/public/.gitkeep -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | ENV["RAILS_ENV"] = "test" 2 | $:.unshift File.dirname(__FILE__) 3 | $:.unshift File.expand_path('../../lib', __FILE__) 4 | 5 | require "scenario/config/environment" 6 | require 'rspec/rails' 7 | require 'capybara/rspec' 8 | require 'pry' 9 | 10 | require 'database_cleaner/active_record' 11 | 12 | RSpec.configure do |config| 13 | config.before(:suite) do 14 | DatabaseCleaner.strategy = :transaction 15 | DatabaseCleaner.clean_with(:truncation) 16 | end 17 | 18 | config.around(:each) do |example| 19 | DatabaseCleaner.cleaning do 20 | example.run 21 | end 22 | end 23 | end 24 | 25 | # Patching Rack::FakeCAS so that it uses the real, configured service URL as the service param 26 | require 'rack/fake_cas' 27 | class Rack::FakeCAS 28 | protected 29 | 30 | def login_page 31 | <<-EOS 32 | 33 | 34 | 35 | 36 |