rubycas-client 0.11.0 → 0.12.0

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