vibes-rubycas-client 2.3.0.alpha → 2.3.0.alpha2

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile CHANGED
@@ -6,10 +6,11 @@ source "http://rubygems.org"
6
6
  # Add dependencies to develop your gem here.
7
7
  # Include everything needed to run rake, tests, features, etc.
8
8
  group :development do
9
- gem "shoulda", ">= 0"
9
+ gem "riot"
10
+ gem "rr"
10
11
  gem "bundler", "~> 1.0.0"
11
12
  gem "jeweler", "~> 1.6.2"
12
- gem "rcov", ">= 0"
13
+ gem "rcov"
13
14
  end
14
15
 
15
16
  gem "activesupport", "~> 2.3.11"
data/Gemfile.lock CHANGED
@@ -9,7 +9,9 @@ GEM
9
9
  rake
10
10
  rake (0.9.2)
11
11
  rcov (0.9.9)
12
- shoulda (2.11.3)
12
+ riot (0.12.4)
13
+ rr
14
+ rr (1.0.2)
13
15
 
14
16
  PLATFORMS
15
17
  ruby
@@ -19,4 +21,5 @@ DEPENDENCIES
19
21
  bundler (~> 1.0.0)
20
22
  jeweler (~> 1.6.2)
21
23
  rcov
22
- shoulda
24
+ riot
25
+ rr
data/History.txt CHANGED
@@ -1,5 +1,26 @@
1
1
  = RubyCAS-Client Changelog
2
2
 
3
+ == Version 2.3.0 :: Prerelease
4
+
5
+ * New Functionality
6
+ * Add configuration option to expect complex extra attributes to be encoded
7
+ in json instead of yaml
8
+ * Split out storage mechanism for single sign out and proxy ticket storage so
9
+ that it is modular
10
+
11
+ * Changes to existing functionality
12
+ * Change gem building from hoe to jeweler
13
+ * expect extra attributes to be nested under a cas:attributes elemenet to
14
+ improve compatibility with other extra attribute implementations
15
+ * Unauthorized requests to URLs ending in .json now show an JSON formatted
16
+ response
17
+
18
+ * Bug Fixes
19
+ * Fixed bug introduced by upstream patch that broke proxy ticket validation
20
+ when using extra attributes
21
+ * Fixed bug where extra attributes key was set on the session with a null
22
+ value when faking with no extra attributes
23
+
3
24
  == Version 2.2.1 :: 2010-06-24
4
25
 
5
26
  * Removed a 3rd party patch to the logging mechanism that broke the client under
data/README.rdoc CHANGED
@@ -1,13 +1,13 @@
1
1
  = RubyCAS-Client
2
2
 
3
- Author:: Matt Zukowski <matt AT roughest DOT net>; inspired by code by Ola Bini <ola.bini AT ki DOT se> and Matt Walker <mwalker AT tamu DOT edu>
3
+ Authors:: Matt Campbell and Rahul Joshi, forked from original project by Matt Zukowski <matt AT roughest DOT net>; inspired by code by Ola Bini <ola.bini AT ki DOT se> and Matt Walker <mwalker AT tamu DOT edu>
4
4
  Copyright:: Portions contributed by Matt Zukowski are copyright (c) 2009 Urbacon Ltd.
5
+ Protions contributed by Matt Campbell and Rahul Joshi are copyright (c) 2011 Vibes Media LLC.
5
6
  Other portions are copyright of their respective authors.
6
7
  License:: MIT License
7
- Websites:: http://github.com/gunark/rubycas-client
8
- http://code.google.com/p/rubycas-client
9
- http://rubyforge.org/projects/rubycas-client
10
-
8
+ Websites:: http://github.com/vibes/rubycas-client
9
+ http://github.com/vibes/rubycas-client/wiki
10
+ http://rubydoc.info/github/vibes/rubycas-client/master/frames
11
11
 
12
12
 
13
13
  === RubyCAS-Client is a Ruby client library for Yale's Central Authentication Service (CAS) protocol.
@@ -234,7 +234,6 @@ In your <tt>config/environment.rb</tt>:
234
234
 
235
235
  CASClient::Frameworks::Rails::Filter.configure(
236
236
  :cas_base_url => "https://cas.example.foo/",
237
- :proxy_retrieval_url => "https://cas-proxy-callback.example.foo/cas_proxy_callback/retrieve_pgt",
238
237
  :proxy_callback_url => "https://cas-proxy-callback.example.foo/cas_proxy_callback/receive_pgt",
239
238
  :logger => cas_logger
240
239
  )
@@ -254,12 +253,9 @@ but your Rails server wouldn't respond to the CAS server's callback until the CA
254
253
 
255
254
  The simplest workaround is this:
256
255
 
257
- 1. Create an empty rails app (i.e. something like <tt>rails cas_proxy_callback</tt>)
258
- 2. Make sure that you have the CAS plugin installed. If you installed it as a gem, you don't have to do anything since
259
- it is already installed. If you want to install as a plugin, see the instructions in the "Installing" section above.
260
- 3. Make sure that the server is up and running, and configure your proxy_callback_url and proxy_retrieval_url to point
261
- to the new server as described above (or rather, make Pound point to the new server, if that's how you're handling https).
262
-
256
+ Run rails using a server that handles multiple concurrent requests. In development, you can use Phusion Passenger Standalone,
257
+ POW (http://pow.cx/), unicorn and many others. In production, I imagine you already support multiple concurrent requests.
258
+
263
259
  That's it. The proxy_callback_controller doesn't require any additional configuration. It doesn't access the database
264
260
  or anything of that sort.
265
261
 
@@ -315,6 +311,12 @@ In your test or Cucumber step definition, simply fake out CAS.
315
311
  This functionality was present in the original version of this plugin.
316
312
  The value of the username is stored in session[:cas_user] (or the user specified field) and session[:casfilteruser] for backwards-compatibility.
317
313
 
314
+ If you need to fake out extra attributes, you can do so like this:
315
+
316
+ CASClient::Frameworks::Rails::Filter.fake("homer", {:role => "user", :email => "homer@test.foo"})
317
+
318
+ And the extra attributes will get put in the proper place in the session.
319
+
318
320
  == License
319
321
 
320
322
  RubyCAS-Client is licensed for use under the terms of the MIT License.
data/Rakefile CHANGED
@@ -27,8 +27,8 @@ Jeweler::RubygemsDotOrgTasks.new
27
27
 
28
28
  require 'rake/testtask'
29
29
  Rake::TestTask.new(:test) do |test|
30
- test.libs << 'lib' << 'test'
31
- test.pattern = 'test/**/test_*.rb'
30
+ test.libs << 'test'
31
+ test.pattern = 'test/**/*_test.rb'
32
32
  test.verbose = true
33
33
  end
34
34
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 2.3.0.alpha
1
+ 2.3.0.alpha2
@@ -3,6 +3,7 @@ module CASClient
3
3
  class Client
4
4
  attr_reader :cas_base_url
5
5
  attr_reader :log, :username_session_key, :extra_attributes_session_key
6
+ attr_reader :ticket_store
6
7
  attr_writer :login_url, :validate_url, :proxy_url, :logout_url, :service_url
7
8
  attr_accessor :proxy_callback_url, :proxy_retrieval_url
8
9
 
@@ -15,6 +16,12 @@ module CASClient
15
16
 
16
17
  raise ArgumentError, "Missing :cas_base_url parameter!" unless conf[:cas_base_url]
17
18
 
19
+ if conf.has_key?("encode_extra_attributes_as")
20
+ unless (conf[:encode_extra_attributes_as] == :json || conf[:encode_extra_attributes_as] == :yaml)
21
+ raise ArgumentError, "Unkown Value for :encode_extra_attributes_as parameter! Allowed options are json or yaml - #{conf[:encode_extra_attributes_as]}"
22
+ end
23
+ end
24
+
18
25
  @cas_base_url = conf[:cas_base_url].gsub(/\/$/, '')
19
26
 
20
27
  @login_url = conf[:login_url]
@@ -24,13 +31,16 @@ module CASClient
24
31
  @service_url = conf[:service_url]
25
32
  @force_ssl_verification = conf[:force_ssl_verification]
26
33
  @proxy_callback_url = conf[:proxy_callback_url]
27
- @proxy_retrieval_url = conf[:proxy_retrieval_url]
28
34
 
29
35
  @username_session_key = conf[:username_session_key] || :cas_user
30
36
  @extra_attributes_session_key = conf[:extra_attributes_session_key] || :cas_extra_attributes
37
+ @ticket_store_class = conf[:ticket_store] || CASClient::Tickets::Storage::LocalDirTicketStore
38
+ @ticket_store = @ticket_store_class.new conf[:ticket_store_config]
39
+ raise CASException, "The Ticket Store is not a subclass of AbstractTicketStore, it is a #{@ticket_store_class}" unless @ticket_store.kind_of? CASClient::Tickets::Storage::AbstractTicketStore
31
40
 
32
41
  @log = CASClient::LoggerWrapper.new
33
42
  @log.set_real_logger(conf[:logger]) if conf[:logger]
43
+ @conf_options = conf
34
44
  end
35
45
 
36
46
  def login_url
@@ -181,28 +191,11 @@ module CASClient
181
191
  end
182
192
 
183
193
  def retrieve_proxy_granting_ticket(pgt_iou)
184
- uri = URI.parse(proxy_retrieval_url)
185
- uri.query = (uri.query ? uri.query + "&" : "") + "pgtIou=#{CGI.escape(pgt_iou)}"
186
- retrieve_url = uri.to_s
187
-
188
- log.debug "Retrieving PGT for PGT IOU #{pgt_iou.inspect} from #{retrieve_url.inspect}"
189
-
190
- # https = Net::HTTP.new(uri.host, uri.port)
191
- # https.use_ssl = (uri.scheme == 'https')
192
- # res = https.post(uri.path, ';')
193
- uri = URI.parse(uri) unless uri.kind_of? URI
194
- https = Net::HTTP.new(uri.host, uri.port)
195
- https.use_ssl = (uri.scheme == 'https')
196
- https.verify_mode = (@force_ssl_verification ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE)
197
-
198
- res = https.start do |conn|
199
- conn.get("#{uri.path}?#{uri.query}")
200
- end
201
-
202
-
203
- raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
204
-
205
- ProxyGrantingTicket.new(res.body.strip, pgt_iou)
194
+ pgt = @ticket_store.retrieve_pgt(pgt_iou)
195
+
196
+ raise CASException, "Couldn't find pgt for pgt_iou #{pgt_iou}" unless pgt
197
+
198
+ ProxyGrantingTicket.new(pgt, pgt_iou)
206
199
  end
207
200
 
208
201
  def add_service_to_login_url(service_url)
@@ -214,7 +207,7 @@ module CASClient
214
207
  private
215
208
  # Fetches a CAS response of the given type from the given URI.
216
209
  # Type should be either ValidationResponse or ProxyResponse.
217
- def request_cas_response(uri, type)
210
+ def request_cas_response(uri, type, options={})
218
211
  log.debug "Requesting CAS response for URI #{uri}"
219
212
 
220
213
  uri = URI.parse(uri) unless uri.kind_of? URI
@@ -240,8 +233,8 @@ module CASClient
240
233
  log.error "CAS server responded with an error! (#{raw_res.inspect})"
241
234
  raise "The CAS authentication server at #{uri} responded with an error (#{raw_res.inspect})!"
242
235
  end
243
-
244
- type.new(raw_res.body)
236
+
237
+ type.new(raw_res.body, @conf_options)
245
238
  end
246
239
 
247
240
  # Submits some data to the given URI and returns a Net::HTTPResponse.
@@ -24,6 +24,11 @@ class CasProxyCallbackController < ActionController::Base
24
24
  render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
25
25
 
26
26
  # TODO: pstore contents should probably be encrypted...
27
+
28
+ casclient = CASClient::Frameworks::Rails::Filter.client
29
+
30
+ casclient.ticket_store.save_pgt_iou(pgtIou, pgtId)
31
+
27
32
  pstore = open_pstore
28
33
 
29
34
  pstore.transaction do
@@ -33,37 +38,6 @@ class CasProxyCallbackController < ActionController::Base
33
38
  render :text => "PGT received. Thank you!" and return
34
39
  end
35
40
 
36
- # Retreives a proxy granting ticket, sends it to output, and deletes the pgt from session storage.
37
- # Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole --
38
- # in fact, the action will not work if the request is not made via SSL or is not local (we allow for local
39
- # non-SSL requests since this allows for the use of reverse HTTPS proxies like Pound).
40
- def retrieve_pgt
41
- #render_error "You can only retrieve PGTs via HTTPS or local connections." and return unless
42
- # request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
43
-
44
- pgtIou = params['pgtIou']
45
-
46
- render_error "No pgtIou specified. Cannot retreive the pgtId." and return unless pgtIou
47
-
48
- pstore = open_pstore
49
-
50
- pgt = nil
51
- pstore.transaction do
52
- pgt = pstore[pgtIou]
53
- end
54
-
55
- if not pgt
56
- render_error "Invalid pgtIou specified. Perhaps this pgt has already been retrieved?" and return
57
- end
58
-
59
- render :text => pgt
60
-
61
- # TODO: need to periodically clean the storage, otherwise it will just keep growing
62
- pstore.transaction do
63
- pstore.delete pgtIou
64
- end
65
- end
66
-
67
41
  private
68
42
  def render_error(msg)
69
43
  # Note that the error messages are mostly just for debugging, since the CAS server never reads them.
@@ -18,11 +18,10 @@ module CASClient
18
18
  if @@fake_user
19
19
  controller.session[client.username_session_key] = @@fake_user
20
20
  controller.session[:casfilteruser] = @@fake_user
21
- controller.session[client.extra_attributes_session_key] = @@fake_extra_attributes
21
+ controller.session[client.extra_attributes_session_key] = @@fake_extra_attributes if @@fake_extra_attributes
22
22
  return true
23
23
  end
24
24
 
25
-
26
25
  last_st = controller.session[:cas_last_valid_ticket]
27
26
 
28
27
  if single_sign_out(controller)
@@ -79,7 +78,7 @@ module CASClient
79
78
  controller.session[:casfilteruser] = vr.user
80
79
 
81
80
  if config[:enable_single_sign_out]
82
- f = store_service_session_lookup(st, controller.request.session_options[:id] || controller.session.session_id)
81
+ f = @@client.ticket_store.store_service_session_lookup(st, controller.request.session_options[:id] || controller.session.session_id)
83
82
  log.debug("Wrote service session lookup file to #{f.inspect} with session id #{controller.request.session_options[:id] || controller.session.session_id.inspect}.")
84
83
  end
85
84
  end
@@ -219,17 +218,25 @@ module CASClient
219
218
  def logout(controller, service = nil)
220
219
  referer = service || controller.request.referer
221
220
  st = controller.session[:cas_last_valid_ticket]
222
- delete_service_session_lookup(st) if st
221
+ @@client.ticket_store.delete_service_session_lookup(st) if st
223
222
  controller.send(:reset_session)
224
223
  controller.send(:redirect_to, client.logout_url(referer))
225
224
  end
226
225
 
227
226
  def unauthorized!(controller, vr = nil)
228
- if controller.params[:format] == "xml"
227
+ format = controller.request.format.to_sym
228
+ format = (format == :js ? :json : format)
229
+ case format
230
+ when :xml, :json
229
231
  if vr
230
- controller.send(:render, :xml => "<errors><error>#{vr.failure_message}</error></errors>", :status => 401)
232
+ case format
233
+ when :xml
234
+ controller.send(:render, :xml => { :error => vr.failure_message }.to_xml(:root => 'errors'), :status => :unauthorized)
235
+ when :json
236
+ controller.send(:render, :json => { :errors => { :error => vr.failure_message }}, :status => :unauthorized)
237
+ end
231
238
  else
232
- controller.send(:head, 401)
239
+ controller.send(:head, :unauthorized)
233
240
  end
234
241
  else
235
242
  redirect_to_cas_for_authentication(controller)
@@ -302,7 +309,7 @@ module CASClient
302
309
 
303
310
 
304
311
  if current_sess_store == required_sess_store
305
- session_id = read_service_session_lookup(si)
312
+ session_id = @@client.ticket_store.read_service_session_lookup(si)
306
313
 
307
314
  if session_id
308
315
  session = current_sess_store::Session.find_by_session_id(session_id)
@@ -362,46 +369,6 @@ module CASClient
362
369
  log.debug("Guessed service url: #{service_url.inspect}")
363
370
  return service_url
364
371
  end
365
-
366
- # Creates a file in tmp/sessions linking a SessionTicket
367
- # with the local Rails session id. The file is named
368
- # cas_sess.<session ticket> and its text contents is the corresponding
369
- # Rails session id.
370
- # Returns the filename of the lookup file created.
371
- def store_service_session_lookup(st, sid)
372
- st = st.ticket if st.kind_of? ServiceTicket
373
- f = File.new(filename_of_service_session_lookup(st), 'w')
374
- f.write(sid)
375
- f.close
376
- return f.path
377
- end
378
-
379
- # Returns the local Rails session ID corresponding to the given
380
- # ServiceTicket. This is done by reading the contents of the
381
- # cas_sess.<session ticket> file created in a prior call to
382
- # #store_service_session_lookup.
383
- def read_service_session_lookup(st)
384
- st = st.ticket if st.kind_of? ServiceTicket
385
- ssl_filename = filename_of_service_session_lookup(st)
386
- return File.exists?(ssl_filename) && IO.read(ssl_filename)
387
- end
388
-
389
- # Removes a stored relationship between a ServiceTicket and a local
390
- # Rails session id. This should be called when the session is being
391
- # closed.
392
- #
393
- # See #store_service_session_lookup.
394
- def delete_service_session_lookup(st)
395
- st = st.ticket if st.kind_of? ServiceTicket
396
- ssl_filename = filename_of_service_session_lookup(st)
397
- File.delete(ssl_filename) if File.exists?(ssl_filename)
398
- end
399
-
400
- # Returns the path and filename of the service session lookup file.
401
- def filename_of_service_session_lookup(st)
402
- st = st.ticket if st.kind_of? ServiceTicket
403
- return "#{RAILS_ROOT}/tmp/sessions/cas_sess.#{st}"
404
- end
405
372
  end
406
373
  end
407
374
 
@@ -31,7 +31,8 @@ module CASClient
31
31
 
32
32
  attr_reader :protocol, :user, :pgt_iou, :proxies, :extra_attributes
33
33
 
34
- def initialize(raw_text)
34
+ def initialize(raw_text, options={})
35
+ conf_options = options
35
36
  parse(raw_text)
36
37
  end
37
38
 
@@ -66,23 +67,22 @@ module CASClient
66
67
  end
67
68
 
68
69
  @extra_attributes = {}
69
- @xml.elements.to_a('//cas:authenticationSuccess/*').each do |el|
70
+ @xml.elements.to_a('//cas:authenticationSuccess/cas:attributes/*').each do |el|
70
71
  # generating the hash requires prefixes to be defined, so add all of the namespaces
71
72
  el.namespaces.each {|k,v| el.add_namespace(k,v)}
72
- @extra_attributes.merge!(Hash.from_xml(el.to_s)) unless ([cas_user, @xml.elements["cas:proxyGrantingTicket"], @xml.elements["cas:proxies"]].include?(el))
73
+ @extra_attributes.merge!(Hash.from_xml(el.to_s))
73
74
  end
74
75
 
75
76
  # unserialize extra attributes
76
77
  @extra_attributes.each do |k, v|
77
- Rails.logger.debug "#{k.inspect} => #{v.inspect}"
78
78
  if v.blank?
79
79
  @extra_attributes[k] = nil
80
+ elsif v.kind_of?(String)
81
+ @extra_attributes[k] = v
82
+ elsif conf.has_key?('encode_extra_attributes_as') && conf_options[:encode_extra_attributes_as] == :json
83
+ @extra_attributes[k] = JSON.parse(v)
80
84
  else
81
- begin
82
- @extra_attributes[k] = JSON.parse(v)
83
- rescue JSON::ParserError => e
84
- @extra_attributes[k] = v
85
- end
85
+ @extra_attributes[k] = YAML.load(v)
86
86
  end
87
87
  end
88
88
  elsif is_failure?
@@ -92,9 +92,8 @@ module CASClient
92
92
  # this should never happen, since the response should already have been recognized as invalid
93
93
  raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
94
94
  end
95
-
96
95
  end
97
-
96
+
98
97
  def is_success?
99
98
  (instance_variable_defined?(:@valid) && @valid) || (protocol > 1.0 && xml.name == "authenticationSuccess")
100
99
  end
@@ -111,7 +110,7 @@ module CASClient
111
110
 
112
111
  attr_reader :proxy_ticket
113
112
 
114
- def initialize(raw_text)
113
+ def initialize(raw_text, options={})
115
114
  parse(raw_text)
116
115
  end
117
116
 
@@ -149,7 +148,7 @@ module CASClient
149
148
  attr_reader :tgt, :ticket, :service_redirect_url
150
149
  attr_reader :failure_message
151
150
 
152
- def initialize(http_response = nil)
151
+ def initialize(http_response = nil, options={})
153
152
  parse_http_response(http_response) if http_response
154
153
  end
155
154
 
@@ -0,0 +1,131 @@
1
+ module CASClient
2
+ module Tickets
3
+ module Storage
4
+ class AbstractTicketStore
5
+ def store_service_session_lookup(st, sid)
6
+ raise 'Implement this in a subclass!'
7
+ end
8
+
9
+ def read_servcie_session_lookup(st)
10
+ raise 'Implement this in a subclass!'
11
+ end
12
+
13
+ def delete_service_session_lookup(st)
14
+ raise 'Implement this in a subclass!'
15
+ end
16
+
17
+ def save_pgt_iou(pgt_iou, pgt)
18
+ raise 'Implement this in a subclass!'
19
+ end
20
+
21
+ def retrieve_pgt(pgt_iou)
22
+ raise 'Implement this in a subclass!'
23
+ end
24
+ end
25
+
26
+ # A Ticket Store that keeps it's tickets in a directory on the local filesystem.
27
+ # Service tickets are stored under tmp/sessions by default
28
+ # and Proxy Granting Tickets and their IOUs are stored in tmp/cas_pgt.pstore
29
+ # This Ticket Store works fine for small sites but will most likely have
30
+ # concurrency problems under heavy load. It also requires that all your
31
+ # worker processes have access to a shared file system.
32
+ #
33
+ # This ticket store takes the following config parameters
34
+ # :storage_dir - The directory to store data in. Defaults to RAILS_ROOT/tmp
35
+ # :service_session_lookup_dir - The directory to store Service Ticket/Session ID files in. Defaults to :storage_dir/sessions
36
+ # :pgt_store_path - The location to store the pgt PStore file. Defaults to :storage_dir/cas_pgt.pstore
37
+ class LocalDirTicketStore < AbstractTicketStore
38
+ require 'pstore'
39
+
40
+ def initialize(config={})
41
+ config ||= {}
42
+ @tmp_dir = config[:storage_dir] || "#{RAILS_ROOT}/tmp"
43
+ @service_session_lookup_dir = config[:service_session_lookup_dir] || "#{@tmp_dir}/sessions"
44
+ @pgt_store_path = config[:pgt_store_path] || "#{@tmp_dir}/cas_pgt.pstore"
45
+ end
46
+
47
+ # Creates a file in tmp/sessions linking a SessionTicket
48
+ # with the local Rails session id. The file is named
49
+ # cas_sess.<session ticket> and its text contents is the corresponding
50
+ # Rails session id.
51
+ # Returns the filename of the lookup file created.
52
+ def store_service_session_lookup(st, sid)
53
+ raise CASException, "No service_ticket specified." unless st
54
+ raise CASException, "No session_id specified." unless sid
55
+
56
+ st = st.ticket if st.kind_of? ServiceTicket
57
+ f = File.new(filename_of_service_session_lookup(st), 'w')
58
+ f.write(sid)
59
+ f.close
60
+ return f.path
61
+ end
62
+
63
+ # Returns the local Rails session ID corresponding to the given
64
+ # ServiceTicket. This is done by reading the contents of the
65
+ # cas_sess.<session ticket> file created in a prior call to
66
+ # #store_service_session_lookup.
67
+ def read_service_session_lookup(st)
68
+ raise CASException, "No service_ticket specified." unless st
69
+
70
+ st = st.ticket if st.kind_of? ServiceTicket
71
+ ssl_filename = filename_of_service_session_lookup(st)
72
+ return File.exists?(ssl_filename) && IO.read(ssl_filename)
73
+ end
74
+
75
+ # Removes a stored relationship between a ServiceTicket and a local
76
+ # Rails session id. This should be called when the session is being
77
+ # closed.
78
+ #
79
+ # See #store_service_session_lookup.
80
+ def delete_service_session_lookup(st)
81
+ raise CASException, "No service_ticket specified." unless st
82
+
83
+ st = st.ticket if st.kind_of? ServiceTicket
84
+ ssl_filename = filename_of_service_session_lookup(st)
85
+ File.delete(ssl_filename) if File.exists?(ssl_filename)
86
+ end
87
+
88
+ def save_pgt_iou(pgt_iou, pgt)
89
+ # TODO: pstore contents should probably be encrypted...
90
+ pstore = open_pstore
91
+
92
+ pstore.transaction do
93
+ pstore[pgt_iou] = pgt
94
+ end
95
+ end
96
+
97
+ def retrieve_pgt(pgt_iou)
98
+ raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
99
+
100
+ pstore = open_pstore
101
+
102
+ pgt = nil
103
+ pstore.transaction do
104
+ pgt = pstore[pgt_iou]
105
+ end
106
+
107
+ raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgt
108
+
109
+ # TODO: need to periodically clean the storage, otherwise it will just keep growing
110
+ pstore.transaction do
111
+ pstore.delete pgt_iou
112
+ end
113
+
114
+ pgt
115
+ end
116
+
117
+ private
118
+
119
+ # Returns the path and filename of the service session lookup file.
120
+ def filename_of_service_session_lookup(st)
121
+ st = st.ticket if st.kind_of? ServiceTicket
122
+ return "#{@service_session_lookup_dir}/cas_sess.#{st}"
123
+ end
124
+
125
+ def open_pstore
126
+ PStore.new(@pgt_store_path)
127
+ end
128
+ end
129
+ end
130
+ end
131
+ end
data/lib/casclient.rb CHANGED
@@ -66,6 +66,7 @@ end
66
66
  require 'casclient/tickets'
67
67
  require 'casclient/responses'
68
68
  require 'casclient/client'
69
+ require 'casclient/tickets/storage'
69
70
 
70
71
  # Detect legacy configuration and show appropriate error message
71
72
  module CAS
data/test/teststrap.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'rubygems'
2
+ require 'bundler/setup'
3
+ require 'casclient'
4
+ require 'riot'
5
+ require 'riot/rr'
6
+
7
+ Riot.reporter = Riot::VerboseStoryReporter
@@ -0,0 +1,32 @@
1
+ require 'teststrap'
2
+ require 'casclient/frameworks/rails/filter'
3
+
4
+ context CASClient::Frameworks::Rails::Filter do
5
+ helper(:controller_with_session) do |session|
6
+ controller = Object.new
7
+ stub(controller).session {session}
8
+ controller
9
+ end
10
+ setup do
11
+ CASClient::Frameworks::Rails::Filter.configure(
12
+ :cas_base_url => 'http://test.local/',
13
+ :logger => stub!
14
+ )
15
+ end
16
+ context "that has fake called with a username" do
17
+ setup { CASClient::Frameworks::Rails::Filter.fake('tester@test.com') }
18
+ should 'set the session user on #filter' do
19
+ setup { Hash.new }
20
+ CASClient::Frameworks::Rails::Filter.filter(controller_with_session(topic))
21
+ topic
22
+ end.equals :cas_user => 'tester@test.com', :casfilteruser => 'tester@test.com'
23
+ end
24
+ context "that has fake called with a username and attributes" do
25
+ setup { CASClient::Frameworks::Rails::Filter.fake('tester@test.com', {:test => 'stuff', :this => 'that'}) }
26
+ should 'set the session user and attributes on #filter' do
27
+ setup { Hash.new }
28
+ CASClient::Frameworks::Rails::Filter.filter(controller_with_session(topic))
29
+ topic
30
+ end.equals :cas_user => 'tester@test.com', :casfilteruser => 'tester@test.com', :cas_extra_attributes => {:test => 'stuff', :this => 'that' }
31
+ end
32
+ end
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{vibes-rubycas-client}
8
- s.version = "2.3.0.alpha"
8
+ s.version = "2.3.0.alpha2"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new("> 1.3.1") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Matt Campbell", "Rahul Joshi", "Matt Zukowski", "Matt Walker"]
12
- s.date = %q{2011-06-09}
12
+ s.date = %q{2011-06-11}
13
13
  s.description = %q{We've taken the rubycas-client and added some enterprisey features and improved compatibility with JASIG's CAS server}
14
14
  s.extra_rdoc_files = [
15
15
  "LICENSE.txt",
@@ -17,7 +17,6 @@ Gem::Specification.new do |s|
17
17
  ]
18
18
  s.files = [
19
19
  ".rvmrc",
20
- ".source_index",
21
20
  "CHANGELOG.txt",
22
21
  "Gemfile",
23
22
  "Gemfile.lock",
@@ -57,13 +56,14 @@ Gem::Specification.new do |s|
57
56
  "examples/rails/script/server",
58
57
  "lib/casclient.rb",
59
58
  "lib/casclient/client.rb",
60
- "lib/casclient/frameworks/merb/filter.rb",
61
- "lib/casclient/frameworks/merb/strategy.rb",
62
59
  "lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb",
63
60
  "lib/casclient/frameworks/rails/filter.rb",
64
61
  "lib/casclient/responses.rb",
65
62
  "lib/casclient/tickets.rb",
63
+ "lib/casclient/tickets/storage.rb",
66
64
  "lib/vibes-rubycas-client.rb",
65
+ "test/teststrap.rb",
66
+ "test/units/casclient/frameworks/rails/filter_test.rb",
67
67
  "vibes-rubycas-client.gemspec"
68
68
  ]
69
69
  s.homepage = %q{http://github.com/vibes/rubycas-client}
@@ -78,20 +78,23 @@ Gem::Specification.new do |s|
78
78
 
79
79
  if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
80
80
  s.add_runtime_dependency(%q<activesupport>, ["~> 2.3.11"])
81
- s.add_development_dependency(%q<shoulda>, [">= 0"])
81
+ s.add_development_dependency(%q<riot>, [">= 0"])
82
+ s.add_development_dependency(%q<rr>, [">= 0"])
82
83
  s.add_development_dependency(%q<bundler>, ["~> 1.0.0"])
83
84
  s.add_development_dependency(%q<jeweler>, ["~> 1.6.2"])
84
85
  s.add_development_dependency(%q<rcov>, [">= 0"])
85
86
  else
86
87
  s.add_dependency(%q<activesupport>, ["~> 2.3.11"])
87
- s.add_dependency(%q<shoulda>, [">= 0"])
88
+ s.add_dependency(%q<riot>, [">= 0"])
89
+ s.add_dependency(%q<rr>, [">= 0"])
88
90
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
89
91
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
90
92
  s.add_dependency(%q<rcov>, [">= 0"])
91
93
  end
92
94
  else
93
95
  s.add_dependency(%q<activesupport>, ["~> 2.3.11"])
94
- s.add_dependency(%q<shoulda>, [">= 0"])
96
+ s.add_dependency(%q<riot>, [">= 0"])
97
+ s.add_dependency(%q<rr>, [">= 0"])
95
98
  s.add_dependency(%q<bundler>, ["~> 1.0.0"])
96
99
  s.add_dependency(%q<jeweler>, ["~> 1.6.2"])
97
100
  s.add_dependency(%q<rcov>, [">= 0"])
metadata CHANGED
@@ -1,14 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vibes-rubycas-client
3
3
  version: !ruby/object:Gem::Version
4
- hash: -1851332218
4
+ hash: -3702664408
5
5
  prerelease: 6
6
6
  segments:
7
7
  - 2
8
8
  - 3
9
9
  - 0
10
10
  - alpha
11
- version: 2.3.0.alpha
11
+ - 2
12
+ version: 2.3.0.alpha2
12
13
  platform: ruby
13
14
  authors:
14
15
  - Matt Campbell
@@ -19,7 +20,7 @@ autorequire:
19
20
  bindir: bin
20
21
  cert_chain: []
21
22
 
22
- date: 2011-06-09 00:00:00 -05:00
23
+ date: 2011-06-11 00:00:00 -05:00
23
24
  default_executable:
24
25
  dependencies:
25
26
  - !ruby/object:Gem::Dependency
@@ -49,12 +50,26 @@ dependencies:
49
50
  segments:
50
51
  - 0
51
52
  version: "0"
52
- name: shoulda
53
+ name: riot
53
54
  version_requirements: *id002
54
55
  prerelease: false
55
56
  - !ruby/object:Gem::Dependency
56
57
  type: :development
57
58
  requirement: &id003 !ruby/object:Gem::Requirement
59
+ none: false
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ hash: 3
64
+ segments:
65
+ - 0
66
+ version: "0"
67
+ name: rr
68
+ version_requirements: *id003
69
+ prerelease: false
70
+ - !ruby/object:Gem::Dependency
71
+ type: :development
72
+ requirement: &id004 !ruby/object:Gem::Requirement
58
73
  none: false
59
74
  requirements:
60
75
  - - ~>
@@ -66,11 +81,11 @@ dependencies:
66
81
  - 0
67
82
  version: 1.0.0
68
83
  name: bundler
69
- version_requirements: *id003
84
+ version_requirements: *id004
70
85
  prerelease: false
71
86
  - !ruby/object:Gem::Dependency
72
87
  type: :development
73
- requirement: &id004 !ruby/object:Gem::Requirement
88
+ requirement: &id005 !ruby/object:Gem::Requirement
74
89
  none: false
75
90
  requirements:
76
91
  - - ~>
@@ -82,11 +97,11 @@ dependencies:
82
97
  - 2
83
98
  version: 1.6.2
84
99
  name: jeweler
85
- version_requirements: *id004
100
+ version_requirements: *id005
86
101
  prerelease: false
87
102
  - !ruby/object:Gem::Dependency
88
103
  type: :development
89
- requirement: &id005 !ruby/object:Gem::Requirement
104
+ requirement: &id006 !ruby/object:Gem::Requirement
90
105
  none: false
91
106
  requirements:
92
107
  - - ">="
@@ -96,7 +111,7 @@ dependencies:
96
111
  - 0
97
112
  version: "0"
98
113
  name: rcov
99
- version_requirements: *id005
114
+ version_requirements: *id006
100
115
  prerelease: false
101
116
  description: We've taken the rubycas-client and added some enterprisey features and improved compatibility with JASIG's CAS server
102
117
  email:
@@ -109,7 +124,6 @@ extra_rdoc_files:
109
124
  - README.rdoc
110
125
  files:
111
126
  - .rvmrc
112
- - .source_index
113
127
  - CHANGELOG.txt
114
128
  - Gemfile
115
129
  - Gemfile.lock
@@ -149,13 +163,14 @@ files:
149
163
  - examples/rails/script/server
150
164
  - lib/casclient.rb
151
165
  - lib/casclient/client.rb
152
- - lib/casclient/frameworks/merb/filter.rb
153
- - lib/casclient/frameworks/merb/strategy.rb
154
166
  - lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb
155
167
  - lib/casclient/frameworks/rails/filter.rb
156
168
  - lib/casclient/responses.rb
157
169
  - lib/casclient/tickets.rb
170
+ - lib/casclient/tickets/storage.rb
158
171
  - lib/vibes-rubycas-client.rb
172
+ - test/teststrap.rb
173
+ - test/units/casclient/frameworks/rails/filter_test.rb
159
174
  - vibes-rubycas-client.gemspec
160
175
  has_rdoc: true
161
176
  homepage: http://github.com/vibes/rubycas-client
data/.source_index DELETED
Binary file
@@ -1,105 +0,0 @@
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
@@ -1,110 +0,0 @@
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
-