rubycas-client 2.0.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.txt +1 -114
- data/History.txt +162 -0
- data/Manifest.txt +8 -1
- data/{README.txt → README.rdoc} +76 -29
- data/Rakefile +7 -0
- data/examples/merb/README.textile +12 -0
- data/examples/merb/Rakefile +35 -0
- data/examples/merb/merb.thor +2020 -0
- data/examples/merb/merb_auth_cas.rb +67 -0
- data/examples/merb/spec/spec_helper.rb +24 -0
- data/lib/casclient/client.rb +65 -16
- data/lib/casclient/frameworks/merb/filter.rb +105 -0
- data/lib/casclient/frameworks/merb/strategy.rb +110 -0
- data/lib/casclient/frameworks/rails/filter.rb +220 -35
- data/lib/casclient/responses.rb +9 -0
- data/lib/casclient/version.rb +2 -2
- metadata +22 -5
@@ -0,0 +1,67 @@
|
|
1
|
+
# run very flat apps with merb -I <app file>.
|
2
|
+
|
3
|
+
# Uncomment for DataMapper ORM
|
4
|
+
# use_orm :datamapper
|
5
|
+
|
6
|
+
# Uncomment for ActiveRecord ORM
|
7
|
+
# use_orm :activerecord
|
8
|
+
|
9
|
+
# Uncomment for Sequel ORM
|
10
|
+
# use_orm :sequel
|
11
|
+
|
12
|
+
$:.unshift(File.dirname(__FILE__) / ".." / ".." / "lib")
|
13
|
+
require "casclient"
|
14
|
+
require 'casclient/frameworks/merb/filter'
|
15
|
+
#
|
16
|
+
# ==== Pick what you test with
|
17
|
+
#
|
18
|
+
|
19
|
+
# This defines which test framework the generators will use.
|
20
|
+
# RSpec is turned on by default.
|
21
|
+
#
|
22
|
+
# To use Test::Unit, you need to install the merb_test_unit gem.
|
23
|
+
# To use RSpec, you don't have to install any additional gems, since
|
24
|
+
# merb-core provides support for RSpec.
|
25
|
+
#
|
26
|
+
# use_test :test_unit
|
27
|
+
use_test :rspec
|
28
|
+
|
29
|
+
#
|
30
|
+
# ==== Choose which template engine to use by default
|
31
|
+
#
|
32
|
+
|
33
|
+
# Merb can generate views for different template engines, choose your favourite as the default.
|
34
|
+
|
35
|
+
use_template_engine :erb
|
36
|
+
# use_template_engine :haml
|
37
|
+
|
38
|
+
Merb::Config.use { |c|
|
39
|
+
c[:framework] = { :public => [Merb.root / "public", nil] }
|
40
|
+
c[:session_store] = 'cookie'
|
41
|
+
c[:exception_details] = true
|
42
|
+
c[:log_level] = :debug # or error, warn, info or fatal
|
43
|
+
c[:log_stream] = STDOUT
|
44
|
+
c[:session_secret_key] = '9f30c015f2132d217bfb81e31668a74fadbdf672'
|
45
|
+
c[:log_file] = Merb.root / "log" / "merb.log"
|
46
|
+
|
47
|
+
c[:reload_classes] = true
|
48
|
+
c[:reload_templates] = true
|
49
|
+
}
|
50
|
+
|
51
|
+
|
52
|
+
Merb::Plugins.config[:"rubycas-client"] = {
|
53
|
+
:cas_base_url => "http://localhost:7777"
|
54
|
+
}
|
55
|
+
|
56
|
+
Merb::Router.prepare do
|
57
|
+
match('/').to(:controller => 'merb_auth_cas', :action =>'index').name(:default)
|
58
|
+
end
|
59
|
+
|
60
|
+
class MerbAuthCas < Merb::Controller
|
61
|
+
include CASClient::Frameworks::Merb::Filter
|
62
|
+
before :cas_filter
|
63
|
+
|
64
|
+
def index
|
65
|
+
"Hi, #{session[:cas_user]}"
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
|
3
|
+
# Add the local gems dir if found within the app root; any dependencies loaded
|
4
|
+
# hereafter will try to load from the local gems before loading system gems.
|
5
|
+
if (local_gem_dir = File.join(File.dirname(__FILE__), '..', 'gems')) && $BUNDLE.nil?
|
6
|
+
$BUNDLE = true; Gem.clear_paths; Gem.path.unshift(local_gem_dir)
|
7
|
+
end
|
8
|
+
|
9
|
+
require "spec"
|
10
|
+
require "merb-core"
|
11
|
+
|
12
|
+
Merb::Config.use do |c|
|
13
|
+
c[:session_store] = "memory"
|
14
|
+
end
|
15
|
+
|
16
|
+
Merb.start_environment(:testing => true,
|
17
|
+
:adapter => 'runner',
|
18
|
+
:environment => ENV['MERB_ENV'] || 'test')
|
19
|
+
|
20
|
+
Spec::Runner.configure do |config|
|
21
|
+
config.include(Merb::Test::ViewHelper)
|
22
|
+
config.include(Merb::Test::RouteHelper)
|
23
|
+
config.include(Merb::Test::ControllerHelper)
|
24
|
+
end
|
data/lib/casclient/client.rb
CHANGED
@@ -11,6 +11,8 @@ module CASClient
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def configure(conf)
|
14
|
+
#TODO: raise error if conf contains unrecognized cas options (this would help detect user typos in the config)
|
15
|
+
|
14
16
|
raise ArgumentError, "Missing :cas_base_url parameter!" unless conf[:cas_base_url]
|
15
17
|
|
16
18
|
@cas_base_url = conf[:cas_base_url].gsub(/\/$/, '')
|
@@ -43,20 +45,31 @@ module CASClient
|
|
43
45
|
# If a logout_url has not been explicitly configured,
|
44
46
|
# the default is cas_base_url + "/logout".
|
45
47
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
#
|
50
|
-
#
|
51
|
-
#
|
52
|
-
|
48
|
+
# destination_url:: Set this if you want the user to be
|
49
|
+
# able to immediately log back in. Generally
|
50
|
+
# you'll want to use something like <tt>request.referer</tt>.
|
51
|
+
# Note that the above behaviour describes RubyCAS-Server
|
52
|
+
# -- other CAS server implementations might use this
|
53
|
+
# parameter differently (or not at all).
|
54
|
+
# follow_url:: This satisfies section 2.3.1 of the CAS protocol spec.
|
55
|
+
# See http://www.ja-sig.org/products/cas/overview/protocol
|
56
|
+
def logout_url(destination_url = nil, follow_url = nil)
|
53
57
|
url = @logout_url || (cas_base_url + "/logout")
|
54
58
|
|
55
|
-
if
|
59
|
+
if destination_url
|
60
|
+
# if present, remove the 'ticket' parameter from the destination_url
|
61
|
+
duri = URI.parse(destination_url)
|
62
|
+
h = duri.query ? query_to_hash(duri.query) : {}
|
63
|
+
h.delete('ticket')
|
64
|
+
duri.query = hash_to_query(h)
|
65
|
+
destination_url = duri.to_s.gsub(/\?$/, '')
|
66
|
+
end
|
67
|
+
|
68
|
+
if destination_url || follow_url
|
56
69
|
uri = URI.parse(url)
|
57
70
|
h = uri.query ? query_to_hash(uri.query) : {}
|
58
|
-
h['
|
59
|
-
h['url'] =
|
71
|
+
h['destination'] = destination_url if destination_url
|
72
|
+
h['url'] = follow_url if follow_url
|
60
73
|
uri.query = hash_to_query(h)
|
61
74
|
uri.to_s
|
62
75
|
else
|
@@ -83,6 +96,30 @@ module CASClient
|
|
83
96
|
end
|
84
97
|
alias validate_proxy_ticket validate_service_ticket
|
85
98
|
|
99
|
+
# Returns true if the configured CAS server is up and responding;
|
100
|
+
# false otherwise.
|
101
|
+
def cas_server_is_up?
|
102
|
+
uri = URI.parse(login_url)
|
103
|
+
|
104
|
+
log.debug "Checking if CAS server at URI '#{uri}' is up..."
|
105
|
+
|
106
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
107
|
+
https.use_ssl = (uri.scheme == 'https')
|
108
|
+
|
109
|
+
begin
|
110
|
+
raw_res = https.start do |conn|
|
111
|
+
conn.get("#{uri.path}?#{uri.query}")
|
112
|
+
end
|
113
|
+
rescue Errno::ECONNREFUSED => e
|
114
|
+
log.warn "CAS server did not respond! (#{e.inspect})"
|
115
|
+
return false
|
116
|
+
end
|
117
|
+
|
118
|
+
log.debug "CAS server responded with #{raw_res.inspect}:\n#{raw_res.body}"
|
119
|
+
|
120
|
+
return raw_res.kind_of?(Net::HTTPSuccess)
|
121
|
+
end
|
122
|
+
|
86
123
|
# Requests a login using the given credentials for the given service;
|
87
124
|
# returns a LoginResponse object.
|
88
125
|
def login_to_service(credentials, service)
|
@@ -167,18 +204,30 @@ module CASClient
|
|
167
204
|
# Fetches a CAS response of the given type from the given URI.
|
168
205
|
# Type should be either ValidationResponse or ProxyResponse.
|
169
206
|
def request_cas_response(uri, type)
|
170
|
-
log.debug "Requesting CAS response
|
207
|
+
log.debug "Requesting CAS response for URI #{uri}"
|
171
208
|
|
172
209
|
uri = URI.parse(uri) unless uri.kind_of? URI
|
173
210
|
https = Net::HTTP.new(uri.host, uri.port)
|
174
211
|
https.use_ssl = (uri.scheme == 'https')
|
175
|
-
raw_res = https.start do |conn|
|
176
|
-
conn.get("#{uri.path}?#{uri.query}")
|
177
|
-
end
|
178
212
|
|
179
|
-
|
213
|
+
begin
|
214
|
+
raw_res = https.start do |conn|
|
215
|
+
conn.get("#{uri.path}?#{uri.query}")
|
216
|
+
end
|
217
|
+
rescue Errno::ECONNREFUSED => e
|
218
|
+
log.error "CAS server did not respond! (#{e.inspect})"
|
219
|
+
raise "The CAS authentication server at #{uri} is not responding!"
|
220
|
+
end
|
180
221
|
|
181
|
-
|
222
|
+
# We accept responses of type 422 since RubyCAS-Server generates these
|
223
|
+
# in response to requests from the client that are processable but contain
|
224
|
+
# invalid CAS data (for example an invalid service ticket).
|
225
|
+
if raw_res.kind_of?(Net::HTTPSuccess) || raw_res.code.to_i == 422
|
226
|
+
log.debug "CAS server responded with #{raw_res.inspect}:\n#{raw_res.body}"
|
227
|
+
else
|
228
|
+
log.error "CAS server responded with an error! (#{raw_res.inspect})"
|
229
|
+
raise "The CAS authentication server at #{uri} responded with an error (#{raw_res.inspect})!"
|
230
|
+
end
|
182
231
|
|
183
232
|
type.new(raw_res.body)
|
184
233
|
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
module CASClient
|
2
|
+
module Frameworks
|
3
|
+
module Merb
|
4
|
+
module Filter
|
5
|
+
attr_reader :client
|
6
|
+
|
7
|
+
def cas_filter
|
8
|
+
@client ||= CASClient::Client.new(config)
|
9
|
+
|
10
|
+
service_ticket = read_ticket(self)
|
11
|
+
|
12
|
+
cas_login_url = client.add_service_to_login_url(read_service_url(self))
|
13
|
+
|
14
|
+
last_service_ticket = session[:cas_last_valid_ticket]
|
15
|
+
if (service_ticket && last_service_ticket &&
|
16
|
+
last_service_ticket.ticket == service_ticket.ticket &&
|
17
|
+
last_service_ticket.service == service_ticket.service)
|
18
|
+
|
19
|
+
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
20
|
+
# The only time when this is acceptable is if the user manually does a refresh and the ticket
|
21
|
+
# happens to be in the URL.
|
22
|
+
log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
|
23
|
+
service_ticket = last_service_ticket
|
24
|
+
elsif last_service_ticket &&
|
25
|
+
!config[:authenticate_on_every_request] &&
|
26
|
+
session[client.username_session_key]
|
27
|
+
# Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
|
28
|
+
# previously authenticated for this service). This is to prevent redirection to the CAS server on every
|
29
|
+
# request.
|
30
|
+
# This behaviour can be disabled (so that every request is routed through the CAS server) by setting
|
31
|
+
# the :authenticate_on_every_request config option to false.
|
32
|
+
log.debug "Existing local CAS session detected for #{session[client.username_session_key].inspect}. "+
|
33
|
+
"Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
|
34
|
+
service_ticket = last_service_ticket
|
35
|
+
end
|
36
|
+
|
37
|
+
if service_ticket
|
38
|
+
client.validate_service_ticket(service_ticket) unless service_ticket.has_been_validated?
|
39
|
+
validation_response = service_ticket.response
|
40
|
+
|
41
|
+
if service_ticket.is_valid?
|
42
|
+
log.info("Ticket #{service_ticket.inspect} for service #{service_ticket.service.inspect} " +
|
43
|
+
"belonging to user #{validation_response.user.inspect} is VALID.")
|
44
|
+
|
45
|
+
session[client.username_session_key] = validation_response.user
|
46
|
+
session[client.extra_attributes_session_key] = validation_response.extra_attributes
|
47
|
+
|
48
|
+
# Store the ticket in the session to avoid re-validating the same service
|
49
|
+
# ticket with the CAS server.
|
50
|
+
session[:cas_last_valid_ticket] = service_ticket
|
51
|
+
return true
|
52
|
+
else
|
53
|
+
log.warn("Ticket #{service_ticket.ticket.inspect} failed validation -- " +
|
54
|
+
"#{validation_response.failure_code}: #{validation_response.failure_message}")
|
55
|
+
redirect cas_login_url
|
56
|
+
throw :halt
|
57
|
+
end
|
58
|
+
else
|
59
|
+
log.warn("No ticket -- redirecting to #{cas_login_url}")
|
60
|
+
redirect cas_login_url
|
61
|
+
throw :halt
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
# Copied from Rails adapter
|
67
|
+
def read_ticket(controller)
|
68
|
+
ticket = controller.params[:ticket]
|
69
|
+
|
70
|
+
return nil unless ticket
|
71
|
+
|
72
|
+
log.debug("Request contains ticket #{ticket.inspect}.")
|
73
|
+
|
74
|
+
if ticket =~ /^PT-/
|
75
|
+
ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
76
|
+
else
|
77
|
+
ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
# Also copied from Rails adapter
|
82
|
+
def read_service_url(controller)
|
83
|
+
if config[:service_url]
|
84
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
85
|
+
return config[:service_url]
|
86
|
+
end
|
87
|
+
|
88
|
+
params = controller.params.dup
|
89
|
+
params.delete(:ticket)
|
90
|
+
service_url = request.protocol + '://' + request.host / controller.url(params.to_hash.symbolize_keys!)
|
91
|
+
log.debug("Guessed service url: #{service_url.inspect}")
|
92
|
+
return service_url
|
93
|
+
end
|
94
|
+
|
95
|
+
def log
|
96
|
+
::Merb.logger
|
97
|
+
end
|
98
|
+
|
99
|
+
def config
|
100
|
+
::Merb::Plugins.config[:"rubycas-client"]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# The 'cas' strategy attempts to login users based on the CAS protocol
|
2
|
+
# http://www.ja-sig.org/products/cas/overview/background/index.html
|
3
|
+
#
|
4
|
+
# install the rubycas-client gem
|
5
|
+
# http://rubyforge.org/projects/rubycas-client/
|
6
|
+
#
|
7
|
+
require 'casclient'
|
8
|
+
class Merb::Authentication
|
9
|
+
module Strategies
|
10
|
+
class CAS < Merb::Authentication::Strategy
|
11
|
+
|
12
|
+
include CASClient
|
13
|
+
|
14
|
+
def run!
|
15
|
+
@client ||= Client.new(config)
|
16
|
+
|
17
|
+
service_ticket = read_ticket
|
18
|
+
|
19
|
+
cas_login_url = @client.add_service_to_login_url(service_url)
|
20
|
+
|
21
|
+
last_service_ticket = session[:cas_last_valid_ticket]
|
22
|
+
if (service_ticket && last_service_ticket &&
|
23
|
+
last_service_ticket.ticket == service_ticket.ticket &&
|
24
|
+
last_service_ticket.service == service_ticket.service)
|
25
|
+
|
26
|
+
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
27
|
+
# The only time when this is acceptable is if the user manually does a refresh and the ticket
|
28
|
+
# happens to be in the URL.
|
29
|
+
log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
|
30
|
+
service_ticket = last_service_ticket
|
31
|
+
elsif last_service_ticket &&
|
32
|
+
!config[:authenticate_on_every_request] &&
|
33
|
+
session[@client.username_session_key]
|
34
|
+
# Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
|
35
|
+
# previously authenticated for this service). This is to prevent redirection to the CAS server on every
|
36
|
+
# request.
|
37
|
+
# This behaviour can be disabled (so that every request is routed through the CAS server) by setting
|
38
|
+
# the :authenticate_on_every_request config option to false.
|
39
|
+
log.debug "Existing local CAS session detected for #{session[@client.username_session_key].inspect}. "+
|
40
|
+
"Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
|
41
|
+
service_ticket = last_service_ticket
|
42
|
+
end
|
43
|
+
|
44
|
+
if service_ticket
|
45
|
+
@client.validate_service_ticket(service_ticket) unless service_ticket.has_been_validated?
|
46
|
+
validation_response = service_ticket.response
|
47
|
+
|
48
|
+
if service_ticket.is_valid?
|
49
|
+
log.info("Ticket #{service_ticket.inspect} for service #{service_ticket.service.inspect} " +
|
50
|
+
"belonging to user #{validation_response.user.inspect} is VALID.")
|
51
|
+
|
52
|
+
session[@client.username_session_key] = validation_response.user
|
53
|
+
session[@client.extra_attributes_session_key] = validation_response.extra_attributes
|
54
|
+
|
55
|
+
# Store the ticket in the session to avoid re-validating the same service
|
56
|
+
# ticket with the CAS server.
|
57
|
+
session[:cas_last_valid_ticket] = service_ticket
|
58
|
+
return true
|
59
|
+
else
|
60
|
+
log.warn("Ticket #{service_ticket.ticket.inspect} failed validation -- " +
|
61
|
+
"#{validation_response.failure_code}: #{validation_response.failure_message}")
|
62
|
+
redirect!(cas_login_url)
|
63
|
+
return false
|
64
|
+
end
|
65
|
+
else
|
66
|
+
log.warn("No ticket -- redirecting to #{cas_login_url}")
|
67
|
+
redirect!(cas_login_url)
|
68
|
+
return false
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def read_ticket
|
73
|
+
ticket = request.params[:ticket]
|
74
|
+
|
75
|
+
return nil unless ticket
|
76
|
+
|
77
|
+
log.debug("Request contains ticket #{ticket.inspect}.")
|
78
|
+
|
79
|
+
if ticket =~ /^PT-/
|
80
|
+
ProxyTicket.new(ticket, service_url, request.params[:renew])
|
81
|
+
else
|
82
|
+
ServiceTicket.new(ticket, service_url, request.params[:renew])
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def service_url
|
87
|
+
if config[:service_url]
|
88
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
89
|
+
return config[:service_url]
|
90
|
+
end
|
91
|
+
|
92
|
+
params = request.params.dup
|
93
|
+
params.delete(:ticket)
|
94
|
+
service_url = "#{request.protocol}://#{request.host}" + request.path
|
95
|
+
log.debug("Guessed service url: #{service_url.inspect}")
|
96
|
+
return service_url
|
97
|
+
end
|
98
|
+
|
99
|
+
def config
|
100
|
+
::Merb::Plugins.config[:"rubycas-client"]
|
101
|
+
end
|
102
|
+
|
103
|
+
def log
|
104
|
+
::Merb.logger
|
105
|
+
end
|
106
|
+
|
107
|
+
end # CAS
|
108
|
+
end # Strategies
|
109
|
+
end
|
110
|
+
|
@@ -12,17 +12,39 @@ module CASClient
|
|
12
12
|
class << self
|
13
13
|
def filter(controller)
|
14
14
|
raise "Cannot use the CASClient filter because it has not yet been configured." if config.nil?
|
15
|
+
|
16
|
+
last_st = controller.session[:cas_last_valid_ticket]
|
17
|
+
|
18
|
+
if single_sign_out(controller)
|
19
|
+
controller.send(:render, :text => "CAS Single-Sign-Out request intercepted.")
|
20
|
+
return false
|
21
|
+
end
|
15
22
|
|
16
23
|
st = read_ticket(controller)
|
17
24
|
|
18
|
-
|
25
|
+
is_new_session = true
|
19
26
|
|
20
|
-
if st &&
|
27
|
+
if st && last_st &&
|
28
|
+
last_st.ticket == st.ticket &&
|
29
|
+
last_st.service == st.service
|
21
30
|
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
22
|
-
# The only
|
23
|
-
# happens to be in the URL.
|
24
|
-
log.warn("Re-using previously validated ticket since the
|
25
|
-
st =
|
31
|
+
# The only situation where this is acceptable is if the user manually does a refresh and
|
32
|
+
# the same ticket happens to be in the URL.
|
33
|
+
log.warn("Re-using previously validated ticket since the ticket id and service are the same.")
|
34
|
+
st = last_st
|
35
|
+
is_new_session = false
|
36
|
+
elsif last_st &&
|
37
|
+
!config[:authenticate_on_every_request] &&
|
38
|
+
controller.session[client.username_session_key]
|
39
|
+
# Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
|
40
|
+
# previously authenticated for this service). This is to prevent redirection to the CAS server on every
|
41
|
+
# request.
|
42
|
+
# This behaviour can be disabled (so that every request is routed through the CAS server) by setting
|
43
|
+
# the :authenticate_on_every_request config option to false.
|
44
|
+
log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
|
45
|
+
"Previous ticket #{last_st.ticket.inspect} will be re-used."
|
46
|
+
st = last_st
|
47
|
+
is_new_session = false
|
26
48
|
end
|
27
49
|
|
28
50
|
if st
|
@@ -30,30 +52,47 @@ module CASClient
|
|
30
52
|
vr = st.response
|
31
53
|
|
32
54
|
if st.is_valid?
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
55
|
+
if is_new_session
|
56
|
+
log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
|
57
|
+
controller.session[client.username_session_key] = vr.user.dup
|
58
|
+
controller.session[client.extra_attributes_session_key] = HashWithIndifferentAccess.new(vr.extra_attributes.dup)
|
59
|
+
|
60
|
+
if vr.extra_attributes
|
61
|
+
log.debug("Extra user attributes provided along with ticket #{st.ticket.inspect}: #{vr.extra_attributes.inspect}.")
|
62
|
+
end
|
63
|
+
|
64
|
+
# RubyCAS-Client 1.x used :casfilteruser as it's username session key,
|
65
|
+
# so we need to set this here to ensure compatibility with configurations
|
66
|
+
# built around the old client.
|
67
|
+
controller.session[:casfilteruser] = vr.user
|
68
|
+
|
69
|
+
if config[:enable_single_sign_out]
|
70
|
+
f = store_service_session_lookup(st, controller.request.session_options[:id] || controller.session.session_id)
|
71
|
+
log.debug("Wrote service session lookup file to #{f.inspect} with session id #{controller.request.session_options[:id] || controller.session.session_id.inspect}.")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
42
75
|
# Store the ticket in the session to avoid re-validating the same service
|
43
76
|
# ticket with the CAS server.
|
44
77
|
controller.session[:cas_last_valid_ticket] = st
|
45
78
|
|
46
79
|
if vr.pgt_iou
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
80
|
+
unless controller.session[:cas_pgt] && controller.session[:cas_pgt].ticket && controller.session[:cas_pgt].iou == vr.pgt_iou
|
81
|
+
log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
|
82
|
+
pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
|
83
|
+
|
84
|
+
if pgt
|
85
|
+
log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
|
86
|
+
controller.session[:cas_pgt] = pgt
|
87
|
+
# For backwards compatibility with RubyCAS-Client 1.x configurations...
|
88
|
+
controller.session[:casfilterpgt] = pgt
|
89
|
+
else
|
90
|
+
log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
|
91
|
+
end
|
54
92
|
else
|
55
|
-
log.
|
93
|
+
log.info("PGT is present in session and PGT IOU #{vr.pgt_iou} matches the saved PGT IOU. Not retrieving new PGT.")
|
56
94
|
end
|
95
|
+
|
57
96
|
end
|
58
97
|
|
59
98
|
return true
|
@@ -62,16 +101,13 @@ module CASClient
|
|
62
101
|
redirect_to_cas_for_authentication(controller)
|
63
102
|
return false
|
64
103
|
end
|
65
|
-
elsif !config[:authenticate_on_every_request] && controller.session[client.username_session_key]
|
66
|
-
# Don't re-authenticate with the CAS server if we already previously authenticated and the
|
67
|
-
# :authenticate_on_every_request option is disabled (it's disabled by default).
|
68
|
-
log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
|
69
|
-
"User will not be re-authenticated."
|
70
|
-
return true
|
71
104
|
else
|
72
105
|
if returning_from_gateway?(controller)
|
73
106
|
log.info "Returning from CAS gateway without authentication."
|
74
|
-
|
107
|
+
|
108
|
+
# reset, so that we can retry authentication if there is a subsequent request
|
109
|
+
controller.session[:cas_sent_to_gateway] = false
|
110
|
+
|
75
111
|
if use_gatewaying?
|
76
112
|
log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
|
77
113
|
return true
|
@@ -96,13 +132,37 @@ module CASClient
|
|
96
132
|
@@config[:use_gatewaying]
|
97
133
|
end
|
98
134
|
|
99
|
-
|
100
|
-
|
135
|
+
# Returns the login URL for the current controller.
|
136
|
+
# Useful when you want to provide a "Login" link in a GatewayFilter'ed
|
137
|
+
# action.
|
138
|
+
def login_url(controller)
|
139
|
+
service_url = read_service_url(controller)
|
140
|
+
url = client.add_service_to_login_url(service_url)
|
141
|
+
log.debug("Generated login url: #{url}")
|
142
|
+
return url
|
143
|
+
end
|
144
|
+
|
145
|
+
# Clears the given controller's local Rails session, does some local
|
146
|
+
# CAS cleanup, and redirects to the CAS logout page. Additionally, the
|
147
|
+
# <tt>request.referer</tt> value from the <tt>controller</tt> instance
|
148
|
+
# is passed to the CAS server as a 'destination' parameter. This
|
149
|
+
# allows RubyCAS server to provide a follow-up login page allowing
|
150
|
+
# the user to log back in to the service they just logged out from
|
151
|
+
# using a different username and password. Other CAS server
|
152
|
+
# implemenations may use this 'destination' parameter in different
|
153
|
+
# ways.
|
154
|
+
# If given, the optional <tt>service</tt> URL overrides
|
155
|
+
# <tt>request.referer</tt>.
|
156
|
+
def logout(controller, service = nil)
|
157
|
+
referer = service || controller.request.referer
|
158
|
+
st = controller.session[:cas_last_valid_ticket]
|
159
|
+
delete_service_session_lookup(st) if st
|
160
|
+
controller.send(:reset_session)
|
161
|
+
controller.send(:redirect_to, client.logout_url(referer))
|
101
162
|
end
|
102
163
|
|
103
164
|
def redirect_to_cas_for_authentication(controller)
|
104
|
-
|
105
|
-
redirect_url = client.add_service_to_login_url(service_url)
|
165
|
+
redirect_url = login_url(controller)
|
106
166
|
|
107
167
|
if use_gatewaying?
|
108
168
|
controller.session[:cas_sent_to_gateway] = true
|
@@ -111,11 +171,92 @@ module CASClient
|
|
111
171
|
controller.session[:cas_sent_to_gateway] = false
|
112
172
|
end
|
113
173
|
|
174
|
+
if controller.session[:previous_redirect_to_cas] &&
|
175
|
+
controller.session[:previous_redirect_to_cas] > (Time.now - 1.second)
|
176
|
+
log.warn("Previous redirect to the CAS server was less than a second ago. The client at #{controller.request.remote_ip.inspect} may be stuck in a redirection loop!")
|
177
|
+
controller.session[:cas_validation_retry_count] ||= 0
|
178
|
+
|
179
|
+
if controller.session[:cas_validation_retry_count] > 3
|
180
|
+
log.error("Redirection loop intercepted. Client at #{controller.request.remote_ip.inspect} will be redirected back to login page and forced to renew authentication.")
|
181
|
+
redirect_url += "&renew=1&redirection_loop_intercepted=1"
|
182
|
+
end
|
183
|
+
|
184
|
+
controller.session[:cas_validation_retry_count] += 1
|
185
|
+
else
|
186
|
+
controller.session[:cas_validation_retry_count] = 0
|
187
|
+
end
|
188
|
+
controller.session[:previous_redirect_to_cas] = Time.now
|
189
|
+
|
114
190
|
log.debug("Redirecting to #{redirect_url.inspect}")
|
115
191
|
controller.send(:redirect_to, redirect_url)
|
116
192
|
end
|
117
193
|
|
118
194
|
private
|
195
|
+
def single_sign_out(controller)
|
196
|
+
|
197
|
+
# Avoid calling raw_post (which may consume the post body) if
|
198
|
+
# this seems to be a file upload
|
199
|
+
if content_type = controller.request.headers["CONTENT_TYPE"] &&
|
200
|
+
content_type =~ %r{^multipart/}
|
201
|
+
return false
|
202
|
+
end
|
203
|
+
|
204
|
+
if controller.request.post? &&
|
205
|
+
controller.params['logoutRequest'] &&
|
206
|
+
controller.params['logoutRequest'] =~
|
207
|
+
%r{^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)</samlp:SessionIndex>}m
|
208
|
+
# TODO: Maybe check that the request came from the registered CAS server? Although this might be
|
209
|
+
# pointless since it's easily spoofable...
|
210
|
+
si = $~[1]
|
211
|
+
|
212
|
+
unless config[:enable_single_sign_out]
|
213
|
+
log.warn "Ignoring single-sign-out request for CAS session #{si.inspect} because ssout functionality is not enabled (see the :enable_single_sign_out config option)."
|
214
|
+
return false
|
215
|
+
end
|
216
|
+
|
217
|
+
log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}."
|
218
|
+
|
219
|
+
begin
|
220
|
+
required_sess_store = ActiveRecord::SessionStore
|
221
|
+
current_sess_store = ActionController::Base.session_store
|
222
|
+
rescue NameError
|
223
|
+
# for older versions of Rails (prior to 2.3)
|
224
|
+
required_sess_store = CGI::Session::ActiveRecordStore
|
225
|
+
current_sess_store = ActionController::Base.session_options[:database_manager]
|
226
|
+
end
|
227
|
+
|
228
|
+
|
229
|
+
if current_sess_store == required_sess_store
|
230
|
+
session_id = read_service_session_lookup(si)
|
231
|
+
|
232
|
+
if session_id
|
233
|
+
session = current_sess_store::Session.find_by_session_id(session_id)
|
234
|
+
if session
|
235
|
+
session.destroy
|
236
|
+
log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{si.inspect}.")
|
237
|
+
else
|
238
|
+
log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
|
239
|
+
end
|
240
|
+
|
241
|
+
log.info("Single-sign-out for session #{session_id.inspect} completed successfuly.")
|
242
|
+
else
|
243
|
+
log.warn("Couldn't destroy session with SessionIndex #{si} because no corresponding session id could be looked up.")
|
244
|
+
end
|
245
|
+
else
|
246
|
+
log.error "Cannot process logout request because this Rails application's session store is "+
|
247
|
+
" #{current_sess_store.name.inspect}. Single Sign-Out only works with the "+
|
248
|
+
" #{required_sess_store.name.inspect} session store."
|
249
|
+
end
|
250
|
+
|
251
|
+
# Return true to indicate that a single-sign-out request was detected
|
252
|
+
# and that further processing of the request is unnecessary.
|
253
|
+
return true
|
254
|
+
end
|
255
|
+
|
256
|
+
# This is not a single-sign-out request.
|
257
|
+
return false
|
258
|
+
end
|
259
|
+
|
119
260
|
def read_ticket(controller)
|
120
261
|
ticket = controller.params[:ticket]
|
121
262
|
|
@@ -130,6 +271,10 @@ module CASClient
|
|
130
271
|
end
|
131
272
|
end
|
132
273
|
|
274
|
+
def returning_from_gateway?(controller)
|
275
|
+
controller.session[:cas_sent_to_gateway]
|
276
|
+
end
|
277
|
+
|
133
278
|
def read_service_url(controller)
|
134
279
|
if config[:service_url]
|
135
280
|
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
@@ -142,6 +287,46 @@ module CASClient
|
|
142
287
|
log.debug("Guessed service url: #{service_url.inspect}")
|
143
288
|
return service_url
|
144
289
|
end
|
290
|
+
|
291
|
+
# Creates a file in tmp/sessions linking a SessionTicket
|
292
|
+
# with the local Rails session id. The file is named
|
293
|
+
# cas_sess.<session ticket> and its text contents is the corresponding
|
294
|
+
# Rails session id.
|
295
|
+
# Returns the filename of the lookup file created.
|
296
|
+
def store_service_session_lookup(st, sid)
|
297
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
298
|
+
f = File.new(filename_of_service_session_lookup(st), 'w')
|
299
|
+
f.write(sid)
|
300
|
+
f.close
|
301
|
+
return f.path
|
302
|
+
end
|
303
|
+
|
304
|
+
# Returns the local Rails session ID corresponding to the given
|
305
|
+
# ServiceTicket. This is done by reading the contents of the
|
306
|
+
# cas_sess.<session ticket> file created in a prior call to
|
307
|
+
# #store_service_session_lookup.
|
308
|
+
def read_service_session_lookup(st)
|
309
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
310
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
311
|
+
return File.exists?(ssl_filename) && IO.read(ssl_filename)
|
312
|
+
end
|
313
|
+
|
314
|
+
# Removes a stored relationship between a ServiceTicket and a local
|
315
|
+
# Rails session id. This should be called when the session is being
|
316
|
+
# closed.
|
317
|
+
#
|
318
|
+
# See #store_service_session_lookup.
|
319
|
+
def delete_service_session_lookup(st)
|
320
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
321
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
322
|
+
File.delete(ssl_filename) if File.exists?(ssl_filename)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Returns the path and filename of the service session lookup file.
|
326
|
+
def filename_of_service_session_lookup(st)
|
327
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
328
|
+
return "#{RAILS_ROOT}/tmp/sessions/cas_sess.#{st}"
|
329
|
+
end
|
145
330
|
end
|
146
331
|
end
|
147
332
|
|
@@ -152,4 +337,4 @@ module CASClient
|
|
152
337
|
end
|
153
338
|
end
|
154
339
|
end
|
155
|
-
end
|
340
|
+
end
|