rubycas-client 0.11.0 → 0.12.0

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.
data/CHANGES CHANGED
@@ -1,5 +1,23 @@
1
1
  = RubyCAS-Client Changelog
2
2
 
3
+ == Version 0.12.0
4
+
5
+ * Prior to redirecting to the CAS login page, the client now stores the
6
+ current service URI in a session variable. This value is used to
7
+ validate the service ticket after the user comes back from the CAS
8
+ server's login page. This should address issues where redirection
9
+ from the CAS server resulted in a slightly different URI from the original
10
+ one used prior to login redirection (for example due to variations in the
11
+ way routing rules are applied by the server).
12
+ * The client now handles malformed CAS server responses more gracefully.
13
+ This makes debugging a malfunctioning CAS server somewhat easier.
14
+ * When receiving a proxy-granting ticket, the cas_proxy_callback_controller
15
+ can now take a parameter called 'pgt' (which is what ought to be used
16
+ according to the published CAS spec) or 'pgtId' (which is what the JA-SIG
17
+ CAS server uses).
18
+ * Logging has been somewhat quieted down. Many messages that were previously
19
+ logged as INFO are now logged as DEBUG.
20
+
3
21
  == Version 0.11.0
4
22
 
5
23
  * Added this changelog to advise users of major changes to the library.
data/README CHANGED
@@ -30,29 +30,30 @@ Alternatively, the library is also available as a gem, which can be installed by
30
30
 
31
31
  gem install rubycas-client
32
32
 
33
- If your Rails application is under subversion control, you can also install the plugin as an external, which will ensure that
34
- you are always up to date:
33
+ If your Rails application is under Subversion control, you can also install the plugin as an svn:external, which will ensure that
34
+ you always have the latest version of RubyCAS-Client:
35
35
 
36
36
  ./script/plugin install -x http://rubycas-client.googlecode.com/svn/trunk/rubycas-client
37
37
 
38
- Please contact the developers via the {rubyforge.org page}[http://rubyforge.org/projects/rubycas-client] if you have bug fixes
38
+ Please contact the developers via the {RubyForge page}[http://rubyforge.org/projects/rubycas-client] if you have bug fixes
39
39
  or enhancements you would like to contribute back.
40
40
 
41
41
  == Examples
42
42
 
43
- ==== Here is an example of how to use the library in your Rails application:
43
+ ==== Using RubyCAS-Client in Rails controllers
44
44
 
45
- Somewhere in your <tt>config/environment.rb</tt> file add this (assuming that you have RubyCAS-Client installed as a plugin, otherwise
46
- you'll need to <tt>require 'cas_auth'</tt> and <tt>require 'cas_proxy_callback_controller'</tt>):
45
+ <i>Note that from this point on we are assuming that you have a working CAS server up and running at an https URI.</i>
47
46
 
47
+ Somewhere in your <tt>config/environment.rb</tt> file add this:
48
+
49
+ require 'cas_auth'
48
50
  CAS::Filter.cas_base_url = "https://login.example.com/cas"
49
51
 
50
- You will also probably (but not necessarily) need to specify the server name where your CAS-protected app is running:
52
+ You will also need to specify the server name where your CAS-protected app is running (i.e. the hostname of the app you are
53
+ adding CAS protection to, not the CAS server):
51
54
 
52
55
  CAS::Filter.server_name = "yourapplication.example.com:3000"
53
56
 
54
- The above setting might not be necessary if your application is running on the standard port 80.
55
-
56
57
  Then, in your <tt>app/controllers/application.rb</tt> (or in whatever controller you want to add the CAS filter for):
57
58
 
58
59
  before_filter CAS::Filter
@@ -68,7 +69,7 @@ filter. For example:
68
69
  user record from the database), you can append another filter method that checks for this value and does whatever you need
69
70
  it to do.
70
71
 
71
- ==== A more complicated example:
72
+ ==== A more complicated example
72
73
 
73
74
  Here is a more complicated configuration showing most of the configuration options (this does not show proxy options however,
74
75
  which are covered in the next section):
@@ -81,8 +82,10 @@ which are covered in the next section):
81
82
  CAS::Filter.gateway = false # act as cas gateway? see http://www.ja-sig.org/products/cas/overview/protocol
82
83
  CAS::Filter.session_username = :casfilteruser # this is the hash in the session where the authenticated username will be stored
83
84
 
85
+ Note that in this example we explicitly specified the login and validate URLs instead of letting RubyCAS-Client figure them out
86
+ based on <tt>CAS::Filter.cas_base_url</tt>.
84
87
 
85
- ==== How to act as a CAS proxy:
88
+ ==== How to act as a CAS proxy
86
89
 
87
90
  CAS 2.0 has a built-in mechanism that allows a CAS-authenticated application to pass on its authentication to other applications.
88
91
  An example where this is useful might be a portal site, where the user logs in to a central website and then gets forwarded to
data/init.rb CHANGED
@@ -5,13 +5,13 @@ require 'cas_proxy_callback_controller'
5
5
  #CAS::Filter.logger = RAILS_DEFAULT_LOGGER if !RAILS_DEFAULT_LOGGER.nil?
6
6
  #CAS::Filter.logger = config.logger if !config.logger.nil?
7
7
 
8
- CAS::Filter.logger = CAS::Logger.new("#{RAILS_ROOT}/log/cas_client_#{RAILS_ENV}.log", 1024000)
8
+ CAS::Filter.logger = CAS::Logger.new("#{RAILS_ROOT}/log/cas_client_#{RAILS_ENV}.log")
9
9
  CAS::Filter.logger.formatter = CAS::Logger::Formatter.new
10
10
 
11
11
  #if RAILS_ENV == "production"
12
12
  # CAS::Filter.logger.level = Logger::WARN
13
13
  #else
14
- CAS::Filter.logger.level = Logger::DEBUG
14
+ # CAS::Filter.logger.level = Logger::DEBUG
15
15
  #end
16
16
 
17
17
 
data/lib/cas.rb CHANGED
@@ -8,6 +8,8 @@ module CAS
8
8
  end
9
9
  class ValidationException < CASException
10
10
  end
11
+ class MalformedServerResponseException < CASException
12
+ end
11
13
 
12
14
  class Receipt
13
15
  attr_accessor :validate_url, :pgt_iou, :primary_authentication, :proxy_callback_url, :proxy_list, :user_name
@@ -55,14 +57,12 @@ module CAS
55
57
  end
56
58
 
57
59
  class AbstractCASResponse
60
+ attr_reader :error_code, :error_message, :successful_authentication
58
61
 
59
62
  def self.retrieve(uri_str)
60
- # puts uri_str
61
63
  prs = URI.parse(uri_str)
62
- # puts prs.inspect
63
64
  https = Net::HTTP.new(prs.host,prs.port)
64
- # puts https.inspect
65
- https.use_ssl=true
65
+ https.use_ssl = true
66
66
  https.start { |conn|
67
67
  # TODO: make sure that HTTP status code in the response is 200... maybe throw exception if is 500?
68
68
  conn.get("#{prs.path}?#{prs.query}").body.strip
@@ -71,17 +71,24 @@ module CAS
71
71
 
72
72
  protected
73
73
  def parse_unsuccessful(elm)
74
- # puts "unsuccessful"
75
74
  @error_message = elm.text.strip
76
75
  @error_code = elm.attributes["code"].strip
77
76
  @successful_authentication = false
78
77
  end
79
78
 
80
79
  def parse(str)
81
- # puts "parsing... #{str}"
82
- doc = REXML::Document.new str
80
+ begin
81
+ doc = REXML::Document.new str
82
+ rescue REXML::ParseException => e
83
+ raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str}\n\nEXCEPTION:\n#{e}"
84
+ end
85
+
86
+ unless doc.elements && doc.elements["cas:serviceResponse"]
87
+ raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str}\n\nXML DOC:\n#{doc.inspect}"
88
+ end
89
+
83
90
  resp = doc.elements["cas:serviceResponse"].elements[1]
84
- # puts "resp... #{resp.name}"
91
+
85
92
  if successful_response? resp
86
93
  parse_successful(resp)
87
94
  else
@@ -92,7 +99,7 @@ module CAS
92
99
 
93
100
  class ServiceTicketValidator < AbstractCASResponse
94
101
  attr_accessor :validate_url, :proxy_callback_url, :renew, :service_ticket, :service
95
- attr_reader :pgt_iou, :user, :error_code, :error_message, :entire_response, :successful_authentication
102
+ attr_reader :pgt_iou, :user, :entire_response
96
103
 
97
104
  def renewed?
98
105
  renew
@@ -154,7 +161,7 @@ module CAS
154
161
  protected
155
162
  def parse_successful(elm)
156
163
  super(elm)
157
- # puts "proxy_successful"
164
+
158
165
  proxies = elm.elements["cas:proxies"]
159
166
  if proxies
160
167
  proxies.elements.each("cas:proxy") { |prox|
@@ -171,9 +178,7 @@ module CAS
171
178
 
172
179
  def request
173
180
  url_building = "#{proxy_url}#{(url_building =~ /\?/)?'&':'?'}pgt=#{pgt}&targetService=#{CGI.escape(target_service)}"
174
- # puts "REQUESTING:"+url_building
175
181
  @@entire_response = ServiceTicketValidator.retrieve url_building
176
- # puts @@entire_response.to_s
177
182
  parse @@entire_response
178
183
  end
179
184
 
data/lib/cas_auth.rb CHANGED
@@ -106,26 +106,27 @@ module CAS
106
106
  if !@@logout_url && @@login_url =~ %r{^(.+?)/[^/]*$}
107
107
  @@logout_url = "#{$1}/logout"
108
108
  end
109
- logger.info "Created logout url: #{@@logout_url}"
109
+ logger.debug "Created logout url: #{@@logout_url}"
110
110
  end
111
111
 
112
112
  def logout_url(controller)
113
113
  create_logout_url unless @@logout_url
114
114
  url = redirect_url(controller,@@logout_url)
115
- logger.info "Logout url is: #{url}"
115
+ logger.debug "Logout url is: #{url}"
116
116
  url
117
117
  end
118
118
 
119
119
  def logout_url=(url)
120
120
  @@logout_url = url
121
- logger.info "Set logout url to: #{url}"
121
+ logger.debug "Initialized logout url to: #{url}"
122
122
  end
123
123
 
124
124
  def cas_base_url=(url)
125
+ url.gsub!(/\/$/, '')
125
126
  CAS::Filter.login_url = "#{url}/login"
126
127
  CAS::Filter.validate_url = "#{url}/proxyValidate"
127
128
  CAS::Filter.proxy_url = "#{url}/proxy"
128
- logger.info "Set CAS base url to: #{url}"
129
+ logger.debug "Initialized CAS base url to: #{url}"
129
130
  end
130
131
 
131
132
  def fake
@@ -162,17 +163,18 @@ module CAS
162
163
  def filter_r(controller)
163
164
  logger.break
164
165
  logger.info("Using real CAS filter in controller: #{controller}")
165
- session_receipt = controller.session[:casfilterreceipt]
166
- session_ticket = controller.session[:caslastticket]
167
- ticket = controller.params[:ticket]
166
+
167
+ session_receipt = controller.session[:casfilterreceipt]
168
+ session_ticket = controller.session[:caslastticket]
169
+ ticket = controller.params[:ticket]
168
170
 
169
- is_valid = false
170
-
171
- if ticket and (!session_ticket or session_ticket != ticket)
172
- log.info "A ticket parameter was given in the URI: #{ticket} and "+
173
- (!session_ticket ? "there is no previous ticket for this session" :
174
- "the ticket is different than the previous ticket, which was #{session_ticket}")
175
-
171
+ is_valid = false
172
+
173
+ if ticket and (!session_ticket or session_ticket != ticket)
174
+ log.info "A ticket parameter was given in the URI: #{ticket} and "+
175
+ (!session_ticket ? "there is no previous ticket for this session" :
176
+ "the ticket is different than the previous ticket, which was #{session_ticket}")
177
+
176
178
  receipt = get_receipt_for_ticket(ticket, controller)
177
179
 
178
180
  if receipt && validate_receipt(receipt)
@@ -188,16 +190,16 @@ module CAS
188
190
  log.debug("Got PGT #{pgt} for PGT IOU #{receipt.pgt_iou}. This will be stored in the session.")
189
191
  controller.session[:casfilterpgt] = pgt
190
192
  else
191
- log.warning("Failed to retrieve a PGT for PGT IOU #{receipt.pgt_iou}!")
193
+ log.error("Failed to retrieve a PGT for PGT IOU #{receipt.pgt_iou}!")
192
194
  end
193
195
  end
194
196
 
195
197
  is_valid = true
196
198
  else
197
199
  if receipt
198
- log.warn "get_receipt_for_ticket() for ticket #{ticket} did not return a receipt!"
199
- else
200
200
  log.warn "Receipt was invalid for ticket #{ticket}!"
201
+ else
202
+ log.warn "get_receipt_for_ticket() for ticket #{ticket} did not return a receipt!"
201
203
  end
202
204
  end
203
205
 
@@ -241,6 +243,7 @@ module CAS
241
243
  logger.info "This request is successfully CAS authenticated for user #{controller.session[@@session_username]}!"
242
244
  return true
243
245
  else
246
+ controller.session[:service] = service_url(controller)
244
247
  logger.info "This request is NOT CAS authenticated, so we will redirect to the login page at: #{redirect_url(controller)}"
245
248
  controller.send :redirect_to, redirect_url(controller) and return false
246
249
  end
@@ -262,7 +265,7 @@ module CAS
262
265
  if r.proxy_ticket
263
266
  logger.info("Got proxy ticket #{r.proxy_ticket} for service #{r.target_service}")
264
267
  else
265
- logger.warn("Did not receive a proxy ticket for service #{r.target_service}!")
268
+ logger.warn("Did not receive a proxy ticket for service #{r.target_service}! Reason: #{r.error_code}: #{r.error_message}")
266
269
  end
267
270
 
268
271
  return r
@@ -323,7 +326,7 @@ module CAS
323
326
  pv = ProxyTicketValidator.new
324
327
  pv.validate_url = @@validate_url
325
328
  pv.service_ticket = ticket
326
- pv.service = service_url(controller)
329
+ pv.service = controller.session[:service] || service_url(controller)
327
330
  pv.renew = @@renew
328
331
  pv.proxy_callback_url = @@proxy_callback_url
329
332
  receipt = nil
@@ -332,6 +335,9 @@ module CAS
332
335
  receipt = Receipt.new(pv)
333
336
  rescue AuthenticationException => e
334
337
  logger.warn("Getting a receipt for the ProxyTicketValidator threw an exception: #{e}")
338
+ rescue MalformedServerResponseException => e
339
+ logger.error("CAS Server returned malformed response:\n\n#{e}")
340
+ raise e
335
341
  end
336
342
  logger.debug "Receipt is: #{receipt.inspect}"
337
343
  receipt
@@ -340,13 +346,17 @@ module CAS
340
346
  def self.service_url(controller)
341
347
  unclean = @@service_url || guess_service(controller)
342
348
  clean = remove_ticket_from_service_uri(unclean)
343
- logger.debug("Service URI with ticket removed is: #{clean}")
349
+ logger.debug("Service URI without ticket is: #{clean}")
344
350
  clean
345
351
  end
346
352
 
347
353
  # FIXME: this method is really poorly named :(
348
354
  def self.redirect_url(controller,url=@@login_url)
349
- "#{url}?service=#{CGI.escape(service_url(controller))}" + ((@@renew)? "&renew=true":"") + ((@@gateway)? "&gateway=true":"") + ((@@query_string.blank?)? "" : "&"+(@@query_string.collect { |k,v| "#{k}=#{v}"}.join("&")))
355
+ "#{url}?service=#{CGI.escape(service_url(controller))}" +
356
+ ((@@renew)? "&renew=true":"") +
357
+ ((@@gateway)? "&gateway=true":"") +
358
+ ((@@query_string.blank?)? "" : "&" +
359
+ (@@query_string.collect { |k,v| "#{k}=#{v}"}.join("&")))
350
360
  end
351
361
 
352
362
  def self.guess_service(controller)
@@ -14,11 +14,13 @@ class CasProxyCallbackController < ActionController::Base
14
14
  # request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
15
15
 
16
16
  pgtIou = params['pgtIou']
17
- pgtId = params['pgtId']
17
+
18
+ # CAS Protocol spec says that the argument should be called 'pgt', but the JA-SIG CAS server seems to use pgtId.
19
+ # To accomodate this, we check for both parameters, although 'pgt' takes precedence over 'pgtId'.
20
+ pgtId = params['pgt'] || params['pgtId']
18
21
 
19
22
  # We need to render a response with HTTP status code 200 when no pgtIou/pgtId is specified because CAS seems first
20
- # call the action without any parameters (maybe to check if the server responds correctly) and only then again,
21
- # this time with the required params.
23
+ # call the action without any parameters (maybe to check if the server responds correctly)
22
24
  render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
23
25
 
24
26
  # TODO: pstore contents should probably be encrypted...
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.0
3
3
  specification_version: 1
4
4
  name: rubycas-client
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.11.0
7
- date: 2007-01-18 00:00:00 -05:00
6
+ version: 0.12.0
7
+ date: 2007-04-04 00:00:00 -04:00
8
8
  summary: Client library for the CAS single-sign-on protocol.
9
9
  require_paths:
10
10
  - lib