echocas-client 2.1.1 → 2.1.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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