derfred-rubycas-client 2.0.9991 → 2.0.9992
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.
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
module CASClient
|
|
2
|
+
module Frameworks
|
|
3
|
+
module Rails
|
|
4
|
+
class RequestHandler
|
|
5
|
+
|
|
6
|
+
attr_accessor :use_gatewaying
|
|
7
|
+
|
|
8
|
+
def self.handle_request(controller, use_gatewaying=false)
|
|
9
|
+
new(controller, use_gatewaying=false).handle_request
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(controller, use_gatewaying=false)
|
|
13
|
+
@controller = controller
|
|
14
|
+
@use_gatewaying = use_gatewaying
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def handle_request
|
|
18
|
+
return :single_sign_out if single_sign_out?(@controller)
|
|
19
|
+
|
|
20
|
+
st, require_validation = determine_request_context(@controller)
|
|
21
|
+
|
|
22
|
+
if st
|
|
23
|
+
handle_ticket(@controller, st, require_validation)
|
|
24
|
+
else
|
|
25
|
+
handle_no_ticket(@controller)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def config
|
|
30
|
+
CASClient::Frameworks::Rails::Filter.config
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def client
|
|
34
|
+
CASClient::Frameworks::Rails::Filter.client
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def log
|
|
38
|
+
CASClient::Frameworks::Rails::Filter.log
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
# high level request handlers
|
|
43
|
+
def handle_ticket(controller, st, require_validation)
|
|
44
|
+
st = client.validate_service_ticket(st) if require_validation and not st.has_been_validated?
|
|
45
|
+
vr = st.response
|
|
46
|
+
|
|
47
|
+
if !require_validation or st.is_valid?
|
|
48
|
+
setup_new_session(controller, st, vr) if require_validation
|
|
49
|
+
|
|
50
|
+
# Store the ticket in the session to avoid re-validating the same service
|
|
51
|
+
# ticket with the CAS server.
|
|
52
|
+
controller.session[:cas_last_valid_ticket] = st
|
|
53
|
+
|
|
54
|
+
handle_pgt_request(vr) if vr.pgt_iou
|
|
55
|
+
|
|
56
|
+
return :allow
|
|
57
|
+
else
|
|
58
|
+
log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
|
|
59
|
+
return :validation_failed
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def handle_no_ticket(controller)
|
|
64
|
+
if returning_from_gateway?(controller)
|
|
65
|
+
log.info "Returning from CAS gateway without authentication."
|
|
66
|
+
|
|
67
|
+
if use_gatewaying?
|
|
68
|
+
log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
|
|
69
|
+
return :allow
|
|
70
|
+
else
|
|
71
|
+
log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
return :to_login
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def handle_pgt_request(controller)
|
|
79
|
+
unless controller.session[:cas_pgt] && controller.session[:cas_pgt].ticket && controller.session[:cas_pgt].iou == vr.pgt_iou
|
|
80
|
+
log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
|
|
81
|
+
pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
|
|
82
|
+
|
|
83
|
+
if pgt
|
|
84
|
+
log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
|
|
85
|
+
controller.session[:cas_pgt] = pgt
|
|
86
|
+
# For backwards compatibility with RubyCAS-Client 1.x configurations...
|
|
87
|
+
controller.session[:casfilterpgt] = pgt
|
|
88
|
+
else
|
|
89
|
+
log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
|
|
90
|
+
end
|
|
91
|
+
else
|
|
92
|
+
log.info("PGT is present in session and PGT IOU #{vr.pgt_iou} matches the saved PGT IOU. Not retrieving new PGT.")
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# single sign out functionality
|
|
98
|
+
def single_sign_out?(controller)
|
|
99
|
+
# Avoid calling raw_post (which may consume the post body) if
|
|
100
|
+
# this seems to be a file upload
|
|
101
|
+
if content_type = controller.request.headers["CONTENT_TYPE"] &&
|
|
102
|
+
content_type =~ %r{^multipart/}
|
|
103
|
+
return false
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
if controller.request.post? &&
|
|
107
|
+
controller.params['logoutRequest'] &&
|
|
108
|
+
controller.params['logoutRequest'] =~
|
|
109
|
+
%r{^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)</samlp:SessionIndex>}m
|
|
110
|
+
# TODO: Maybe check that the request came from the registered CAS server? Although this might be
|
|
111
|
+
# pointless since it's easily spoofable...
|
|
112
|
+
si = $~[1]
|
|
113
|
+
|
|
114
|
+
unless config[:enable_single_sign_out]
|
|
115
|
+
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)."
|
|
116
|
+
return false
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}."
|
|
120
|
+
|
|
121
|
+
required_sess_store = session_store
|
|
122
|
+
current_sess_store = ActionController::Base.session_options[:database_manager]
|
|
123
|
+
|
|
124
|
+
if current_sess_store == required_sess_store
|
|
125
|
+
session_id = read_service_session_lookup(si)
|
|
126
|
+
|
|
127
|
+
if session_id
|
|
128
|
+
session = session_store::Session.find_by_session_id(session_id)
|
|
129
|
+
if session
|
|
130
|
+
session.destroy
|
|
131
|
+
log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{si.inspect}.")
|
|
132
|
+
else
|
|
133
|
+
log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
log.info("Single-sign-out for session #{session_id.inspect} completed successfuly.")
|
|
137
|
+
else
|
|
138
|
+
log.warn("Couldn't destroy session with SessionIndex #{si} because no corresponding session id could be looked up.")
|
|
139
|
+
end
|
|
140
|
+
else
|
|
141
|
+
log.error "Cannot process logout request because this Rails application's session store is "+
|
|
142
|
+
" #{current_sess_store.name.inspect}. Single Sign-Out only works with the "+
|
|
143
|
+
" #{required_sess_store.name.inspect} session store."
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Return true to indicate that a single-sign-out request was detected
|
|
147
|
+
# and that further processing of the request is unnecessary.
|
|
148
|
+
return true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# This is not a single-sign-out request.
|
|
152
|
+
return false
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def session_store
|
|
156
|
+
if CGI.const_defined?("Session")
|
|
157
|
+
CGI::Session::ActiveRecordStore
|
|
158
|
+
else
|
|
159
|
+
ActiveRecord::SessionStore
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Creates a file in tmp/sessions linking a SessionTicket
|
|
164
|
+
# with the local Rails session id. The file is named
|
|
165
|
+
# cas_sess.<session ticket> and its text contents is the corresponding
|
|
166
|
+
# Rails session id.
|
|
167
|
+
# Returns the filename of the lookup file created.
|
|
168
|
+
def store_service_session_lookup(st, sid)
|
|
169
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
170
|
+
f = File.new(filename_of_service_session_lookup(st), 'w')
|
|
171
|
+
f.write(sid)
|
|
172
|
+
f.close
|
|
173
|
+
return f.path
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Returns the local Rails session ID corresponding to the given
|
|
177
|
+
# ServiceTicket. This is done by reading the contents of the
|
|
178
|
+
# cas_sess.<session ticket> file created in a prior call to
|
|
179
|
+
# #store_service_session_lookup.
|
|
180
|
+
def read_service_session_lookup(st)
|
|
181
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
182
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
|
183
|
+
return File.exists?(ssl_filename) && IO.read(ssl_filename)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Removes a stored relationship between a ServiceTicket and a local
|
|
187
|
+
# Rails session id. This should be called when the session is being
|
|
188
|
+
# closed.
|
|
189
|
+
#
|
|
190
|
+
# See #store_service_session_lookup.
|
|
191
|
+
def delete_service_session_lookup(st)
|
|
192
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
193
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
|
194
|
+
File.delete(ssl_filename) if File.exists?(ssl_filename)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Returns the path and filename of the service session lookup file.
|
|
198
|
+
def filename_of_service_session_lookup(st)
|
|
199
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
200
|
+
return "#{RAILS_ROOT}/tmp/sessions/cas_sess.#{st}"
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def determine_request_context(controller)
|
|
204
|
+
last_st = controller.session[:cas_last_valid_ticket]
|
|
205
|
+
st = read_ticket(controller)
|
|
206
|
+
|
|
207
|
+
require_validation = true
|
|
208
|
+
|
|
209
|
+
if st && last_st &&
|
|
210
|
+
last_st.ticket == st.ticket &&
|
|
211
|
+
last_st.service == st.service
|
|
212
|
+
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
|
213
|
+
# The only situation where this is acceptable is if the user manually does a refresh and
|
|
214
|
+
# the same ticket happens to be in the URL.
|
|
215
|
+
log.warn("Re-using previously validated ticket since the ticket id and service are the same.")
|
|
216
|
+
st = last_st
|
|
217
|
+
require_validation = false
|
|
218
|
+
elsif last_st &&
|
|
219
|
+
!config[:authenticate_on_every_request] &&
|
|
220
|
+
controller.session[client.username_session_key]
|
|
221
|
+
# Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
|
|
222
|
+
# previously authenticated for this service). This is to prevent redirection to the CAS server on every
|
|
223
|
+
# request.
|
|
224
|
+
# This behaviour can be disabled (so that every request is routed through the CAS server) by setting
|
|
225
|
+
# the :authenticate_on_every_request config option to false.
|
|
226
|
+
log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
|
|
227
|
+
"Previous ticket #{last_st.ticket.inspect} will be re-used."
|
|
228
|
+
st = last_st
|
|
229
|
+
require_validation = false
|
|
230
|
+
elsif last_st &&
|
|
231
|
+
config[:authenticate_on_every_request] &&
|
|
232
|
+
controller.session[client.username_session_key]
|
|
233
|
+
st = last_st
|
|
234
|
+
require_validation = true
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
[st, require_validation]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def read_ticket(controller)
|
|
241
|
+
ticket = controller.params[:ticket]
|
|
242
|
+
|
|
243
|
+
return nil unless ticket
|
|
244
|
+
|
|
245
|
+
log.debug("Request contains ticket #{ticket.inspect}.")
|
|
246
|
+
|
|
247
|
+
if ticket =~ /^PT-/
|
|
248
|
+
ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
|
249
|
+
else
|
|
250
|
+
ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def read_service_url(controller)
|
|
255
|
+
if config[:service_url]
|
|
256
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
|
257
|
+
return config[:service_url]
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
params = controller.params.dup
|
|
261
|
+
params.delete(:ticket)
|
|
262
|
+
service_url = controller.url_for(params)
|
|
263
|
+
log.debug("Guessed service url: #{service_url.inspect}")
|
|
264
|
+
return service_url
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
def setup_new_session(controller, st, vr)
|
|
268
|
+
log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
|
|
269
|
+
controller.session[client.username_session_key] = vr.user.dup
|
|
270
|
+
controller.session[client.extra_attributes_session_key] = HashWithIndifferentAccess.new(vr.extra_attributes.dup)
|
|
271
|
+
|
|
272
|
+
if vr.extra_attributes
|
|
273
|
+
log.debug("Extra user attributes provided along with ticket #{st.ticket.inspect}: #{vr.extra_attributes.inspect}.")
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# RubyCAS-Client 1.x used :casfilteruser as it's username session key,
|
|
277
|
+
# so we need to set this here to ensure compatibility with configurations
|
|
278
|
+
# built around the old client.
|
|
279
|
+
controller.session[:casfilteruser] = vr.user
|
|
280
|
+
|
|
281
|
+
if config[:enable_single_sign_out]
|
|
282
|
+
store_service_session_lookup(st, controller.session.session_id)
|
|
283
|
+
log.debug("Wrote service session lookup file to #{f.inspect} with session id #{controller.session.session_id.inspect}.")
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# gatewaying support
|
|
289
|
+
def returning_from_gateway?(controller)
|
|
290
|
+
controller.session[:cas_sent_to_gateway]
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def use_gatewaying?
|
|
294
|
+
@use_gatewaying
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
end
|
|
300
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: derfred-rubycas-client
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.0.
|
|
4
|
+
version: 2.0.9992
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Matt Zukowski
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- lib/casclient.rb
|
|
57
57
|
- lib/casclient/client.rb
|
|
58
58
|
- lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb
|
|
59
|
+
- lib/casclient/frameworks/rails/request_handler.rb
|
|
59
60
|
- lib/casclient/frameworks/rails/filter.rb
|
|
60
61
|
- lib/casclient/frameworks/merb/strategy.rb
|
|
61
62
|
- lib/casclient/responses.rb
|