echocas-client 2.1.1 → 2.1.2

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,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
@@ -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
@@ -3,48 +3,53 @@ module CASClient
3
3
  module Rails
4
4
  class Filter
5
5
  cattr_reader :config, :log, :client
6
-
6
+
7
7
  # These are initialized when you call configure.
8
8
  @@config = nil
9
9
  @@client = nil
10
10
  @@log = nil
11
11
  @@fake_user = nil
12
-
13
-
12
+
13
+
14
14
  class << self
15
15
  def filter(controller)
16
16
  raise "Cannot use the CASClient filter because it has not yet been configured." if config.nil?
17
-
18
-
17
+
18
+ if controller.env['CAS_FILTERED']
19
+ raise "Filtering more than once might lead to redirection loops ! Don't do it (called from #{caller}, previously from #{controller.env['CAS_FILTERED']})"
20
+ else
21
+ controller.env['CAS_FILTERED'] = caller
22
+ end
23
+
19
24
  if @@fake_user
20
25
  controller.session[client.username_session_key] = @@fake_user
21
26
  controller.session[:casfilteruser] = @@fake_user
22
27
  return true
23
28
  end
24
-
25
-
29
+
30
+
26
31
  last_st = controller.session[:cas_last_valid_ticket]
27
-
32
+
28
33
  if single_sign_out(controller)
29
34
  controller.send(:render, :text => "CAS Single-Sign-Out request intercepted.")
30
- return false
35
+ return false
31
36
  end
32
37
 
33
38
  st = read_ticket(controller)
34
-
39
+
35
40
  is_new_session = true
36
-
37
- if st && last_st &&
38
- last_st.ticket == st.ticket &&
41
+
42
+ if st && last_st &&
43
+ last_st.ticket == st.ticket &&
39
44
  last_st.service == st.service
40
- # warn() rather than info() because we really shouldn't be re-validating the same ticket.
41
- # The only situation where this is acceptable is if the user manually does a refresh and
45
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
46
+ # The only situation where this is acceptable is if the user manually does a refresh and
42
47
  # the same ticket happens to be in the URL.
43
48
  log.warn("Re-using previously validated ticket since the ticket id and service are the same.")
44
49
  st = last_st
45
50
  is_new_session = false
46
51
  elsif last_st &&
47
- !config[:authenticate_on_every_request] &&
52
+ !config[:authenticate_on_every_request] &&
48
53
  controller.session[client.username_session_key]
49
54
  # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
50
55
  # previously authenticated for this service). This is to prevent redirection to the CAS server on every
@@ -58,36 +63,36 @@ module CASClient
58
63
  st = last_st
59
64
  is_new_session = false
60
65
  end
61
-
66
+
62
67
  if st
63
68
  client.validate_service_ticket(st) unless st.has_been_validated?
64
69
  vr = st.response
65
-
70
+
66
71
  if st.is_valid?
67
72
  if is_new_session
68
73
  log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
69
74
  controller.session[client.username_session_key] = vr.user.dup
70
75
  controller.session[client.extra_attributes_session_key] = HashWithIndifferentAccess.new(vr.extra_attributes)
71
-
76
+
72
77
  if vr.extra_attributes
73
78
  log.debug("Extra user attributes provided along with ticket #{st.ticket.inspect}: #{vr.extra_attributes.inspect}.")
74
79
  end
75
-
80
+
76
81
  # RubyCAS-Client 1.x used :casfilteruser as it's username session key,
77
82
  # so we need to set this here to ensure compatibility with configurations
78
83
  # built around the old client.
79
84
  controller.session[:casfilteruser] = vr.user
80
-
85
+
81
86
  if config[:enable_single_sign_out]
82
87
  f = store_service_session_lookup(st, controller.request.session_options[:id] || controller.session.session_id)
83
88
  log.debug("Wrote service session lookup file to #{f.inspect} with session id #{controller.request.session_options[:id] || controller.session.session_id.inspect}.")
84
89
  end
85
90
  end
86
-
91
+
87
92
  # Store the ticket in the session to avoid re-validating the same service
88
93
  # ticket with the CAS server.
89
94
  controller.session[:cas_last_valid_ticket] = st
90
-
95
+
91
96
  if vr.pgt_iou
92
97
  unless controller.session[:cas_pgt] && controller.session[:cas_pgt].ticket && controller.session[:cas_pgt].iou == vr.pgt_iou
93
98
  log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
@@ -106,7 +111,7 @@ module CASClient
106
111
  end
107
112
 
108
113
  end
109
-
114
+
110
115
  return true
111
116
  else
112
117
  log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
@@ -127,51 +132,51 @@ module CASClient
127
132
  log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
128
133
  end
129
134
  end
130
-
135
+
131
136
  unauthorized!(controller)
132
137
  return false
133
138
  end
134
139
  end
135
-
140
+
136
141
  def configure(config)
137
142
  @@config = config
138
143
  @@config[:logger] = RAILS_DEFAULT_LOGGER unless @@config[:logger]
139
144
  @@client = CASClient::Client.new(config)
140
145
  @@log = client.log
141
146
  end
142
-
147
+
143
148
  # used to allow faking for testing
144
149
  # with cucumber and other tools.
145
- # use like
150
+ # use like
146
151
  # CASClient::Frameworks::Rails::Filter.fake("homer")
147
152
  def fake(username)
148
153
  @@fake_user = username
149
154
  end
150
-
155
+
151
156
  def use_gatewaying?
152
157
  @@config[:use_gatewaying]
153
158
  end
154
-
155
- # Returns the login URL for the current controller.
159
+
160
+ # Returns the login URL for the current controller.
156
161
  # Useful when you want to provide a "Login" link in a GatewayFilter'ed
157
- # action.
162
+ # action.
158
163
  def login_url(controller)
159
164
  service_url = read_service_url(controller)
160
165
  url = client.add_service_to_login_url(service_url)
161
166
  log.debug("Generated login url: #{url}")
162
167
  return url
163
168
  end
164
-
165
- # Clears the given controller's local Rails session, does some local
169
+
170
+ # Clears the given controller's local Rails session, does some local
166
171
  # CAS cleanup, and redirects to the CAS logout page. Additionally, the
167
- # <tt>request.referer</tt> value from the <tt>controller</tt> instance
168
- # is passed to the CAS server as a 'destination' parameter. This
172
+ # <tt>request.referer</tt> value from the <tt>controller</tt> instance
173
+ # is passed to the CAS server as a 'destination' parameter. This
169
174
  # allows RubyCAS server to provide a follow-up login page allowing
170
- # the user to log back in to the service they just logged out from
171
- # using a different username and password. Other CAS server
172
- # implemenations may use this 'destination' parameter in different
173
- # ways.
174
- # If given, the optional <tt>service</tt> URL overrides
175
+ # the user to log back in to the service they just logged out from
176
+ # using a different username and password. Other CAS server
177
+ # implemenations may use this 'destination' parameter in different
178
+ # ways.
179
+ # If given, the optional <tt>service</tt> URL overrides
175
180
  # <tt>request.referer</tt>.
176
181
  def logout(controller, service = nil)
177
182
  referer = service || controller.request.referer
@@ -180,7 +185,7 @@ module CASClient
180
185
  controller.send(:reset_session)
181
186
  controller.send(:redirect_to, client.logout_url(referer))
182
187
  end
183
-
188
+
184
189
  def unauthorized!(controller, vr = nil)
185
190
  if controller.params[:format] == "xml"
186
191
  if vr
@@ -192,47 +197,47 @@ module CASClient
192
197
  redirect_to_cas_for_authentication(controller)
193
198
  end
194
199
  end
195
-
200
+
196
201
  def redirect_to_cas_for_authentication(controller)
197
202
  redirect_url = login_url(controller)
198
-
203
+
199
204
  if use_gatewaying?
200
205
  controller.session[:cas_sent_to_gateway] = true
201
206
  redirect_url << "&gateway=true"
202
207
  else
203
208
  controller.session[:cas_sent_to_gateway] = false
204
209
  end
205
-
210
+
206
211
  if controller.session[:previous_redirect_to_cas] &&
207
212
  controller.session[:previous_redirect_to_cas] > (Time.now - 1.second)
208
213
  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!")
209
214
  controller.session[:cas_validation_retry_count] ||= 0
210
-
215
+
211
216
  if controller.session[:cas_validation_retry_count] > 3
212
217
  log.error("Redirection loop intercepted. Client at #{controller.request.remote_ip.inspect} will be redirected back to login page and forced to renew authentication.")
213
218
  redirect_url += "&renew=1&redirection_loop_intercepted=1"
214
219
  end
215
-
220
+
216
221
  controller.session[:cas_validation_retry_count] += 1
217
222
  else
218
223
  controller.session[:cas_validation_retry_count] = 0
219
224
  end
220
225
  controller.session[:previous_redirect_to_cas] = Time.now
221
-
226
+
222
227
  log.debug("Redirecting to #{redirect_url.inspect}")
223
228
  controller.send(:redirect_to, redirect_url)
224
229
  end
225
-
230
+
226
231
  private
227
232
  def single_sign_out(controller)
228
-
233
+
229
234
  # Avoid calling raw_post (which may consume the post body) if
230
235
  # this seems to be a file upload
231
236
  if content_type = controller.request.headers["CONTENT_TYPE"] &&
232
237
  content_type =~ %r{^multipart/}
233
238
  return false
234
239
  end
235
-
240
+
236
241
  if controller.request.post? &&
237
242
  controller.params['logoutRequest'] &&
238
243
  controller.params['logoutRequest'] =~
@@ -240,14 +245,14 @@ module CASClient
240
245
  # TODO: Maybe check that the request came from the registered CAS server? Although this might be
241
246
  # pointless since it's easily spoofable...
242
247
  si = $~[1]
243
-
248
+
244
249
  unless config[:enable_single_sign_out]
245
250
  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)."
246
251
  return false
247
252
  end
248
-
253
+
249
254
  log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}."
250
-
255
+
251
256
  begin
252
257
  required_sess_store = ActiveRecord::SessionStore
253
258
  current_sess_store = ActionController::Base.session_store
@@ -269,7 +274,7 @@ module CASClient
269
274
  else
270
275
  log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
271
276
  end
272
-
277
+
273
278
  log.info("Single-sign-out for session #{session_id.inspect} completed successfuly.")
274
279
  else
275
280
  log.warn("Couldn't destroy session with SessionIndex #{si} because no corresponding session id could be looked up.")
@@ -279,50 +284,50 @@ module CASClient
279
284
  " #{current_sess_store.name.inspect}. Single Sign-Out only works with the "+
280
285
  " #{required_sess_store.name.inspect} session store."
281
286
  end
282
-
287
+
283
288
  # Return true to indicate that a single-sign-out request was detected
284
289
  # and that further processing of the request is unnecessary.
285
290
  return true
286
291
  end
287
-
292
+
288
293
  # This is not a single-sign-out request.
289
294
  return false
290
295
  end
291
-
296
+
292
297
  def read_ticket(controller)
293
298
  ticket = controller.params[:ticket]
294
-
299
+
295
300
  return nil unless ticket
296
-
301
+
297
302
  log.debug("Request contains ticket #{ticket.inspect}.")
298
-
303
+
299
304
  if ticket =~ /^PT-/
300
305
  ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
301
306
  else
302
307
  ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
303
308
  end
304
309
  end
305
-
310
+
306
311
  def returning_from_gateway?(controller)
307
312
  controller.session[:cas_sent_to_gateway]
308
313
  end
309
-
314
+
310
315
  def read_service_url(controller)
311
316
  if config[:service_url]
312
317
  log.debug("Using explicitly set service url: #{config[:service_url]}")
313
318
  return config[:service_url]
314
319
  end
315
-
320
+
316
321
  params = controller.params.dup
317
322
  params.delete(:ticket)
318
323
  service_url = controller.url_for(params)
319
324
  log.debug("Guessed service url: #{service_url.inspect}")
320
325
  return service_url
321
326
  end
322
-
327
+
323
328
  # Creates a file in tmp/sessions linking a SessionTicket
324
329
  # with the local Rails session id. The file is named
325
- # cas_sess.<session ticket> and its text contents is the corresponding
330
+ # cas_sess.<session ticket> and its text contents is the corresponding
326
331
  # Rails session id.
327
332
  # Returns the filename of the lookup file created.
328
333
  def store_service_session_lookup(st, sid)
@@ -332,17 +337,17 @@ module CASClient
332
337
  f.close
333
338
  return f.path
334
339
  end
335
-
340
+
336
341
  # Returns the local Rails session ID corresponding to the given
337
342
  # ServiceTicket. This is done by reading the contents of the
338
- # cas_sess.<session ticket> file created in a prior call to
343
+ # cas_sess.<session ticket> file created in a prior call to
339
344
  # #store_service_session_lookup.
340
345
  def read_service_session_lookup(st)
341
346
  st = st.ticket if st.kind_of? ServiceTicket
342
347
  ssl_filename = filename_of_service_session_lookup(st)
343
348
  return File.exists?(ssl_filename) && IO.read(ssl_filename)
344
349
  end
345
-
350
+
346
351
  # Removes a stored relationship between a ServiceTicket and a local
347
352
  # Rails session id. This should be called when the session is being
348
353
  # closed.
@@ -353,7 +358,7 @@ module CASClient
353
358
  ssl_filename = filename_of_service_session_lookup(st)
354
359
  File.delete(ssl_filename) if File.exists?(ssl_filename)
355
360
  end
356
-
361
+
357
362
  # Returns the path and filename of the service session lookup file.
358
363
  def filename_of_service_session_lookup(st)
359
364
  st = st.ticket if st.kind_of? ServiceTicket
@@ -361,7 +366,7 @@ module CASClient
361
366
  end
362
367
  end
363
368
  end
364
-
369
+
365
370
  class GatewayFilter < Filter
366
371
  def self.use_gatewaying?
367
372
  return true unless @@config[:use_gatewaying] == false
@@ -2,7 +2,7 @@ module CASClient #:nodoc:
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 2
4
4
  MINOR = 1
5
- TINY = 0
5
+ TINY = 2
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
8
8
  end