devise_cas_authenticatable 1.0.0.alpha11 → 1.0.0.alpha12

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -1,4 +1,5 @@
1
1
  .bundle/*
2
+ .idea/*
2
3
  .yardoc/*
3
4
  pkg/*
4
5
  spec/scenario/db/*.sqlite3
data/Gemfile.lock CHANGED
@@ -17,7 +17,7 @@ GIT
17
17
  PATH
18
18
  remote: .
19
19
  specs:
20
- devise_cas_authenticatable (1.0.0.alpha11)
20
+ devise_cas_authenticatable (1.0.0.alpha12)
21
21
  devise (>= 1.0.6)
22
22
  rubycas-client (>= 2.2.1)
23
23
 
@@ -69,7 +69,7 @@ GEM
69
69
  configuration (1.2.0)
70
70
  crypt-isaac (0.9.1)
71
71
  culerity (0.2.15)
72
- devise (1.4.5)
72
+ devise (1.4.7)
73
73
  bcrypt-ruby (~> 3.0)
74
74
  orm_adapter (~> 0.0.3)
75
75
  warden (~> 1.0.3)
data/README.md CHANGED
@@ -110,5 +110,4 @@ See also
110
110
  TODO
111
111
  ----
112
112
 
113
- * Implement CAS single sign-off support (maybe via a Rack middleware?)
114
113
  * Test on non-ActiveRecord ORMs
@@ -16,6 +16,11 @@ class Devise::CasSessionsController < Devise::SessionsController
16
16
  end
17
17
 
18
18
  def destroy
19
+ # Delete the ticket->session ID mapping if one exists for this session
20
+ if ticket = session['cas_last_valid_ticket']
21
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.current_strategy.delete_session_index(ticket)
22
+ end
23
+
19
24
  # if :cas_create_user is false a CAS session might be open but not signed_in
20
25
  # in such case we destroy the session here
21
26
  if signed_in?(resource_name)
@@ -29,8 +34,53 @@ class Devise::CasSessionsController < Devise::SessionsController
29
34
  destination << after_sign_out_path_for(resource_name)
30
35
  redirect_to(::Devise.cas_client.logout_url(destination))
31
36
  end
32
-
33
- private
37
+
38
+ def single_sign_out
39
+ if ::Devise.cas_enable_single_sign_out
40
+ session_index = read_session_index
41
+ if session_index
42
+ logger.debug "Intercepted single-sign-out request for CAS session #{session_index}."
43
+ session_id = ::DeviseCasAuthenticatable::SingleSignOut::Strategies.current_strategy.find_session_id_by_index(session_index)
44
+ if session_id
45
+ destroy_cas_session(session_id, session_index)
46
+ end
47
+ else
48
+ logger.warn "Ignoring CAS single-sign-out request as no session index could be parsed from the parameters."
49
+ end
50
+ else
51
+ logger.warn "Ignoring CAS single-sign-out request as feature is not currently enabled."
52
+ end
53
+
54
+ render :nothing => true
55
+ end
56
+
57
+ private
58
+
59
+ def read_session_index
60
+ if request.headers['CONTENT_TYPE'] =~ %r{^multipart/}
61
+ false
62
+ elsif request.post? && params['logoutRequest'] =~
63
+ %r{^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)</samlp:SessionIndex>}m
64
+ $~[1]
65
+ else
66
+ false
67
+ end
68
+ end
69
+
70
+ def destroy_cas_session(session_id, session_index)
71
+ if env[:session_store] && env[:session_store].respond_to?(:destroy_session)
72
+ if env[:session_store].destroy_session(session_id)
73
+ logger.debug "Destroyed session #{session_id} corresponding to service ticket #{session_index}."
74
+ else
75
+ logger.debug "Data for session #{session_id} was not found. It may have already been cleared by a local CAS logout request."
76
+ end
77
+ else
78
+ logger.warn "A single sign out request was received for ticket #{session_index} but the Rails session_store is not a type supported for single-sign-out by devise_cas_authenticatable."
79
+ end
80
+
81
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.current_strategy.delete_session_index(session_index)
82
+ end
83
+
34
84
  def returning_from_cas?
35
85
  params[:ticket] || request.referer =~ /^#{::Devise.cas_client.cas_base_url}/
36
86
  end
@@ -2,10 +2,10 @@
2
2
 
3
3
  Gem::Specification.new do |s|
4
4
  s.name = %q{devise_cas_authenticatable}
5
- s.version = "1.0.0.alpha11"
5
+ s.version = "1.0.0.alpha12"
6
6
 
7
7
  s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
8
- s.authors = ["Nat Budin"]
8
+ s.authors = ["Nat Budin", "Jeremy Haile"]
9
9
  s.description = %q{CAS authentication module for Devise}
10
10
  s.email = %q{natbudin@gmail.com}
11
11
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -7,7 +7,8 @@ if ActionController::Routing.name =~ /ActionDispatch/
7
7
  def devise_cas_authenticatable(mapping, controllers)
8
8
  # service endpoint for CAS server
9
9
  get "service", :to => "#{controllers[:cas_sessions]}#service", :as => "service"
10
-
10
+ post "service", :to => "#{controllers[:cas_sessions]}#single_sign_out", :as => "single_sign_out"
11
+
11
12
  resource :session, :only => [], :controller => controllers[:cas_sessions], :path => "" do
12
13
  get :new, :path => mapping.path_names[:sign_in], :as => "new"
13
14
  get :unregistered
@@ -25,6 +26,7 @@ else
25
26
  def cas_authenticatable(routes, mapping)
26
27
  routes.with_options(:controller => 'devise/cas_sessions', :name_prefix => nil) do |session|
27
28
  session.send(:"#{mapping.name}_service", '/service', :action => 'service', :conditions => {:method => :get})
29
+ session.send(:"#{mapping.name}_service", '/service', :action => 'single_sign_out', :conditions => {:method => :post})
28
30
  session.send(:"unregistered_#{mapping.name}_session", '/unregistered', :action => "unregistered", :conditions => {:method => :get})
29
31
  session.send(:"new_#{mapping.name}_session", mapping.path_names[:sign_in], :action => 'new', :conditions => {:method => :get})
30
32
  session.send(:"#{mapping.name}_session", mapping.path_names[:sign_in], :action => 'create', :conditions => {:method => :post})
@@ -0,0 +1,12 @@
1
+ ActiveRecord::SessionStore.class_eval do
2
+
3
+ include DeviseCasAuthenticatable::SingleSignOut::SetSession
4
+ alias_method_chain :set_session, :storage
5
+
6
+ def destroy_session(sid)
7
+ if session = Session::find_by_session_id(sid)
8
+ session.destroy
9
+ end
10
+ end
11
+
12
+ end
@@ -0,0 +1,27 @@
1
+ require "action_controller/session/redis_session_store"
2
+
3
+ module DeviseCasAuthenticatable
4
+ module SingleSignOut
5
+ module RedisSessionStore
6
+
7
+ include DeviseCasAuthenticatable::SingleSignOut::SetSession
8
+
9
+ def destroy_session(sid)
10
+ @pool.del(sid)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+
17
+ if ::Redis::Store.rails3?
18
+ ActionDispatch::Session::RedisSessionStore.class_eval do
19
+ include DeviseCasAuthenticatable::SingleSignOut::RedisSessionStore
20
+ alias_method_chain :set_session, :storage
21
+ end
22
+ else
23
+ ActionController::Session::RedisSessionStore.class_eval do
24
+ include DeviseCasAuthenticatable::SingleSignOut::RedisSessionStore
25
+ alias_method_chain :set_session, :storage
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class Base
5
+ def logger
6
+ @logger ||= Rails.logger
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,31 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class RailsCache < Base
5
+ def store_session_id_for_index(session_index, session_id)
6
+ logger.debug("Storing #{session_id} for index #{session_index}")
7
+ Rails.cache.write(cache_key(session_index), session_id)
8
+ end
9
+
10
+ def find_session_id_by_index(session_index)
11
+ sid = Rails.cache.read(cache_key(session_index))
12
+ logger.debug("Found session id #{sid} for index #{session_index}")
13
+ sid
14
+ end
15
+
16
+ def delete_session_index(session_index)
17
+ logger.debug("Deleting index #{session_index}")
18
+ Rails.cache.delete(cache_key(session_index))
19
+ end
20
+
21
+ private
22
+
23
+ def cache_key(session_index)
24
+ "devise_cas_authenticatable:#{session_index}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.add( :rails_cache, DeviseCasAuthenticatable::SingleSignOut::Strategies::RailsCache )
@@ -0,0 +1,58 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module Strategies
4
+ class << self
5
+
6
+ # Add a strategy and store it in a hash.
7
+ def add(label, strategy, &block)
8
+ strategy ||= Class.new(DeviseCasAuthenticatable::SingleSignOut::Strategies::Base)
9
+ strategy.class_eval(&block) if block_given?
10
+
11
+ check_method(label, strategy, :store_session_id_for_index)
12
+ check_method(label, strategy, :find_session_id_by_index)
13
+ check_method(label, strategy, :delete_session_index)
14
+
15
+ unless strategy.ancestors.include?(DeviseCasAuthenticatable::SingleSignOut::Strategies::Base)
16
+ raise "#{label.inspect} is not a #{base}"
17
+ end
18
+
19
+ _strategies[label] = strategy.new()
20
+ end
21
+
22
+ # Update a previously given strategy.
23
+ def update(label, &block)
24
+ strategy = _strategies[label]
25
+ raise "Unknown strategy #{label.inspect}" unless strategy
26
+ add(label, strategy, &block)
27
+ end
28
+
29
+ # Provides access to strategies by label
30
+ def [](label)
31
+ _strategies[label]
32
+ end
33
+
34
+ def current_strategy
35
+ self[::Devise.cas_single_sign_out_mapping_strategy]
36
+ end
37
+
38
+ # Clears all declared.
39
+ def clear!
40
+ _strategies.clear
41
+ end
42
+
43
+ private
44
+
45
+ def _strategies
46
+ @strategies ||= {}
47
+ end
48
+
49
+ def check_method(label, strategy, method)
50
+ unless strategy.method_defined?(method)
51
+ raise NoMethodError, "#{method.to_s} is not declared in the #{label.inspect} strategy"
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,30 @@
1
+ module DeviseCasAuthenticatable
2
+ module SingleSignOut
3
+ module SetSession
4
+ def set_session_with_storage(env, sid, session_data)
5
+ if session_data['cas_last_valid_ticket_store']
6
+ ::DeviseCasAuthenticatable::SingleSignOut::Strategies.current_strategy.store_session_id_for_index(session_data['cas_last_valid_ticket'], sid)
7
+ session_data['cas_last_valid_ticket_store'] = nil
8
+ end
9
+
10
+ set_session_without_storage(env, sid, session_data)
11
+ end
12
+ end
13
+ end
14
+ end
15
+
16
+ if defined?(ActionDispatch)
17
+ # Need to make a small extension to rails to expose the session_store to our controllers
18
+ require 'action_dispatch/middleware/session/abstract_store'
19
+ ActionDispatch::Session::AbstractStore.class_eval do
20
+ def prepare_with_session_store!(env)
21
+ prepare_without_session_store!(env)
22
+ env[:session_store] = self
23
+ end
24
+ alias_method_chain :prepare!, :session_store
25
+ end
26
+ end
27
+
28
+ require 'devise_cas_authenticatable/single_sign_out/strategies'
29
+ require 'devise_cas_authenticatable/single_sign_out/strategies/base'
30
+ require 'devise_cas_authenticatable/single_sign_out/strategies/rails_cache'
@@ -17,6 +17,12 @@ module Devise
17
17
  ticket = read_ticket(params)
18
18
  if ticket
19
19
  if resource = mapping.to.authenticate_with_cas_ticket(ticket)
20
+ # Store the ticket in the session for later usage
21
+ if ::Devise.cas_enable_single_sign_out
22
+ session['cas_last_valid_ticket'] = ticket.ticket
23
+ session['cas_last_valid_ticket_store'] = true
24
+ end
25
+
20
26
  success!(resource)
21
27
  elsif ticket.is_valid?
22
28
  redirect!(::Devise.cas_unregistered_url(request.url, mapping), :username => ticket.response.user)
@@ -5,6 +5,16 @@ require 'devise_cas_authenticatable/routes'
5
5
  require 'devise_cas_authenticatable/strategy'
6
6
  require 'devise_cas_authenticatable/exceptions'
7
7
 
8
+ require 'devise_cas_authenticatable/single_sign_out'
9
+
10
+ if defined?(ActiveRecord::SessionStore)
11
+ require 'devise_cas_authenticatable/single_sign_out/session_store/active_record'
12
+ end
13
+
14
+ if defined?(Redis::Store)
15
+ require 'devise_cas_authenticatable/single_sign_out/session_store/redis'
16
+ end
17
+
8
18
  require 'rubycas-client'
9
19
 
10
20
  # Register as a Rails engine if Rails::Engine exists
@@ -31,7 +41,16 @@ module Devise
31
41
 
32
42
  # The login URL of the CAS server. If undefined, will default based on cas_base_url.
33
43
  @@cas_validate_url = nil
34
-
44
+
45
+ # Should devise_cas_authenticatable enable single-sign-out? Requires use of a supported
46
+ # session_store. Currently supports active_record or redis.
47
+ # False by default.
48
+ @@cas_enable_single_sign_out = false
49
+
50
+ # What strategy should single sign out use for tracking token->session ID mapping.
51
+ # :rails_cache by default.
52
+ @@cas_single_sign_out_mapping_strategy = :rails_cache
53
+
35
54
  # Should devise_cas_authenticatable attempt to create new user records for
36
55
  # unknown usernames? True by default.
37
56
  @@cas_create_user = true
@@ -43,12 +62,12 @@ module Devise
43
62
  # Name of the parameter passed in the logout query
44
63
  @@cas_destination_logout_param_name = nil
45
64
 
46
- mattr_accessor :cas_base_url, :cas_login_url, :cas_logout_url, :cas_validate_url, :cas_create_user, :cas_destination_logout_param_name, :cas_username_column
65
+ mattr_accessor :cas_base_url, :cas_login_url, :cas_logout_url, :cas_validate_url, :cas_create_user, :cas_destination_logout_param_name, :cas_username_column, :cas_enable_single_sign_out, :cas_single_sign_out_mapping_strategy
47
66
 
48
67
  def self.cas_create_user?
49
68
  cas_create_user
50
69
  end
51
-
70
+
52
71
  # Return a CASClient::Client instance based on configuration parameters.
53
72
  def self.cas_client
54
73
  @@cas_client ||= CASClient::Client.new(
@@ -56,7 +75,8 @@ module Devise
56
75
  :cas_base_url => @@cas_base_url,
57
76
  :login_url => @@cas_login_url,
58
77
  :logout_url => @@cas_logout_url,
59
- :validate_url => @@cas_validate_url
78
+ :validate_url => @@cas_validate_url,
79
+ :enable_single_sign_out => @@cas_enable_single_sign_out
60
80
  )
61
81
  end
62
82
 
metadata CHANGED
@@ -1,20 +1,21 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: devise_cas_authenticatable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.alpha11
4
+ version: 1.0.0.alpha12
5
5
  prerelease: 6
6
6
  platform: ruby
7
7
  authors:
8
8
  - Nat Budin
9
+ - Jeremy Haile
9
10
  autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2011-09-16 00:00:00.000000000 -04:00
13
+ date: 2011-09-28 00:00:00.000000000 -04:00
13
14
  default_executable:
14
15
  dependencies:
15
16
  - !ruby/object:Gem::Dependency
16
17
  name: devise
17
- requirement: &2158960580 !ruby/object:Gem::Requirement
18
+ requirement: &2157571980 !ruby/object:Gem::Requirement
18
19
  none: false
19
20
  requirements:
20
21
  - - ! '>='
@@ -22,10 +23,10 @@ dependencies:
22
23
  version: 1.0.6
23
24
  type: :runtime
24
25
  prerelease: false
25
- version_requirements: *2158960580
26
+ version_requirements: *2157571980
26
27
  - !ruby/object:Gem::Dependency
27
28
  name: rubycas-client
28
- requirement: &2158959980 !ruby/object:Gem::Requirement
29
+ requirement: &2157571440 !ruby/object:Gem::Requirement
29
30
  none: false
30
31
  requirements:
31
32
  - - ! '>='
@@ -33,10 +34,10 @@ dependencies:
33
34
  version: 2.2.1
34
35
  type: :runtime
35
36
  prerelease: false
36
- version_requirements: *2158959980
37
+ version_requirements: *2157571440
37
38
  - !ruby/object:Gem::Dependency
38
39
  name: rails
39
- requirement: &2158959480 !ruby/object:Gem::Requirement
40
+ requirement: &2157570280 !ruby/object:Gem::Requirement
40
41
  none: false
41
42
  requirements:
42
43
  - - ! '>='
@@ -44,10 +45,10 @@ dependencies:
44
45
  version: 3.0.7
45
46
  type: :development
46
47
  prerelease: false
47
- version_requirements: *2158959480
48
+ version_requirements: *2157570280
48
49
  - !ruby/object:Gem::Dependency
49
50
  name: rspec-rails
50
- requirement: &2158958920 !ruby/object:Gem::Requirement
51
+ requirement: &2157568700 !ruby/object:Gem::Requirement
51
52
  none: false
52
53
  requirements:
53
54
  - - ! '>='
@@ -55,10 +56,10 @@ dependencies:
55
56
  version: 2.6.1
56
57
  type: :development
57
58
  prerelease: false
58
- version_requirements: *2158958920
59
+ version_requirements: *2157568700
59
60
  - !ruby/object:Gem::Dependency
60
61
  name: mocha
61
- requirement: &2158958500 !ruby/object:Gem::Requirement
62
+ requirement: &2157568240 !ruby/object:Gem::Requirement
62
63
  none: false
63
64
  requirements:
64
65
  - - ! '>='
@@ -66,10 +67,10 @@ dependencies:
66
67
  version: '0'
67
68
  type: :development
68
69
  prerelease: false
69
- version_requirements: *2158958500
70
+ version_requirements: *2157568240
70
71
  - !ruby/object:Gem::Dependency
71
72
  name: shoulda
72
- requirement: &2158957800 !ruby/object:Gem::Requirement
73
+ requirement: &2157567540 !ruby/object:Gem::Requirement
73
74
  none: false
74
75
  requirements:
75
76
  - - ! '>='
@@ -77,10 +78,10 @@ dependencies:
77
78
  version: '0'
78
79
  type: :development
79
80
  prerelease: false
80
- version_requirements: *2158957800
81
+ version_requirements: *2157567540
81
82
  - !ruby/object:Gem::Dependency
82
83
  name: sqlite3-ruby
83
- requirement: &2158957240 !ruby/object:Gem::Requirement
84
+ requirement: &2157566640 !ruby/object:Gem::Requirement
84
85
  none: false
85
86
  requirements:
86
87
  - - ! '>='
@@ -88,10 +89,10 @@ dependencies:
88
89
  version: '0'
89
90
  type: :development
90
91
  prerelease: false
91
- version_requirements: *2158957240
92
+ version_requirements: *2157566640
92
93
  - !ruby/object:Gem::Dependency
93
94
  name: sham_rack
94
- requirement: &2158956680 !ruby/object:Gem::Requirement
95
+ requirement: &2157565160 !ruby/object:Gem::Requirement
95
96
  none: false
96
97
  requirements:
97
98
  - - ! '>='
@@ -99,10 +100,10 @@ dependencies:
99
100
  version: '0'
100
101
  type: :development
101
102
  prerelease: false
102
- version_requirements: *2158956680
103
+ version_requirements: *2157565160
103
104
  - !ruby/object:Gem::Dependency
104
105
  name: capybara
105
- requirement: &2158956260 !ruby/object:Gem::Requirement
106
+ requirement: &2157545440 !ruby/object:Gem::Requirement
106
107
  none: false
107
108
  requirements:
108
109
  - - ! '>='
@@ -110,10 +111,10 @@ dependencies:
110
111
  version: '0'
111
112
  type: :development
112
113
  prerelease: false
113
- version_requirements: *2158956260
114
+ version_requirements: *2157545440
114
115
  - !ruby/object:Gem::Dependency
115
116
  name: crypt-isaac
116
- requirement: &2158955460 !ruby/object:Gem::Requirement
117
+ requirement: &2157544760 !ruby/object:Gem::Requirement
117
118
  none: false
118
119
  requirements:
119
120
  - - ! '>='
@@ -121,10 +122,10 @@ dependencies:
121
122
  version: '0'
122
123
  type: :development
123
124
  prerelease: false
124
- version_requirements: *2158955460
125
+ version_requirements: *2157544760
125
126
  - !ruby/object:Gem::Dependency
126
127
  name: launchy
127
- requirement: &2158954660 !ruby/object:Gem::Requirement
128
+ requirement: &2157543980 !ruby/object:Gem::Requirement
128
129
  none: false
129
130
  requirements:
130
131
  - - ! '>='
@@ -132,7 +133,7 @@ dependencies:
132
133
  version: '0'
133
134
  type: :development
134
135
  prerelease: false
135
- version_requirements: *2158954660
136
+ version_requirements: *2157543980
136
137
  description: CAS authentication module for Devise
137
138
  email: natbudin@gmail.com
138
139
  executables: []
@@ -155,6 +156,12 @@ files:
155
156
  - lib/devise_cas_authenticatable/model.rb
156
157
  - lib/devise_cas_authenticatable/routes.rb
157
158
  - lib/devise_cas_authenticatable/schema.rb
159
+ - lib/devise_cas_authenticatable/single_sign_out.rb
160
+ - lib/devise_cas_authenticatable/single_sign_out/session_store/active_record.rb
161
+ - lib/devise_cas_authenticatable/single_sign_out/session_store/redis.rb
162
+ - lib/devise_cas_authenticatable/single_sign_out/strategies.rb
163
+ - lib/devise_cas_authenticatable/single_sign_out/strategies/base.rb
164
+ - lib/devise_cas_authenticatable/single_sign_out/strategies/rails_cache.rb
158
165
  - lib/devise_cas_authenticatable/strategy.rb
159
166
  - rails/init.rb
160
167
  - spec/routes_spec.rb
@@ -203,7 +210,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
210
  version: '0'
204
211
  segments:
205
212
  - 0
206
- hash: 3233699978426426317
213
+ hash: 3607945198939265999
207
214
  required_rubygems_version: !ruby/object:Gem::Requirement
208
215
  none: false
209
216
  requirements: