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.9991
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