rubycas-client 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/CHANGES +18 -0
  2. data/README +22 -2
  3. data/lib/cas.rb +2 -2
  4. data/lib/cas_auth.rb +172 -31
  5. metadata +2 -2
data/CHANGES CHANGED
@@ -1,5 +1,23 @@
1
1
  = RubyCAS-Client Changelog
2
2
 
3
+ == Version 1.1.0 :: 2007-12-21
4
+
5
+ * Fixed serious bug having to do with logouts. You can now end the
6
+ CAS session on the client-side (i.e. force the client to re-authenticate)
7
+ by setting session[:casfilteruser] = nil.
8
+ * Added new GatewayFilter. This is identical to the normal Filter but
9
+ has the gateway option set to true by default. This should make
10
+ using the gateway option easier.
11
+ * The CAS::Filter methods are now properly documented.
12
+ * Simplified guess_service produces better URLs when redirecting to the CAS
13
+ server for authentication and the service URL is not explicitly specified.
14
+ [delagoya]
15
+ * The correct method for overriding the service URL for the client is now
16
+ properly documented. You should use service_url=, as server_name= no longer
17
+ works and instead generates a warning message.
18
+ * logout_url() now takes an additional 'service' parameter. If specified, this
19
+ URL will be passed on to the CAS server as part of the logout URL.
20
+
3
21
  == Version 1.0.0 :: 2007-07-26
4
22
 
5
23
  * RubyCAS-Client has matured to the point where it is probably safe to
data/README CHANGED
@@ -85,6 +85,26 @@ which are covered in the next section):
85
85
  Note that in this example we explicitly specified the login and validate URLs instead of letting RubyCAS-Client figure them out
86
86
  based on <tt>CAS::Filter.cas_base_url</tt>.
87
87
 
88
+ ==== Defining a 'logout' action
89
+
90
+ Your Rails application's controller(s) will probably have some sort of logout function. In it you will likely want reset the
91
+ user's session for your application, and then redirect to the CAS server's logout URL. Here's an example of how to do this:
92
+
93
+ def logout
94
+ reset_session
95
+ redirect_to CAS::Filter.logout_url(self, request.referer)
96
+ end
97
+
98
+ ==== Gatewayed authentication
99
+
100
+ RubyCAS-Client supports gatewaying as of version 1.1.0. Gatewaying allows for optional CAS authentication, so that users who
101
+ already have a pre-existing CAS SSO session will be automatically authenticated for the gatewayed service, while those who
102
+ do not, will be allowed to access the service without authentication. This is useful for example when you want to show
103
+ some additional private content on a homepage to authenticated users, but also want unauthenticated users to be able to
104
+ access the page without first logging in.
105
+
106
+ For more information on using gatewaying, see CAS::GatewayFilter.
107
+
88
108
  ==== How to act as a CAS proxy
89
109
 
90
110
  CAS 2.0 has a built-in mechanism that allows a CAS-authenticated application to pass on its authentication to other applications.
@@ -189,7 +209,7 @@ Then make sure the library for open SSL is installed. For example, on an Debian/
189
209
  == License
190
210
 
191
211
  This program is free software; you can redistribute it and/or modify
192
- it under the terms of the GNU General Public License as published by
212
+ it under the terms of the GNU Lesser General Public License as published by
193
213
  the Free Software Foundation; either version 2 of the License, or
194
214
  (at your option) any later version.
195
215
 
@@ -198,6 +218,6 @@ but WITHOUT ANY WARRANTY; without even the implied warranty of
198
218
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
199
219
  GNU General Public License for more details.
200
220
 
201
- You should have received a copy of the GNU General Public License
221
+ You should have received a copy of the GNU Lesser General Public License
202
222
  along with this program (see the file called LICENSE); if not, write to the
203
223
  Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
data/lib/cas.rb CHANGED
@@ -80,11 +80,11 @@ module CAS
80
80
  begin
81
81
  doc = REXML::Document.new str
82
82
  rescue REXML::ParseException => e
83
- raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str}\n\nEXCEPTION:\n#{e}"
83
+ raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str.inspect}\n\nEXCEPTION:\n#{e}"
84
84
  end
85
85
 
86
86
  unless doc.elements && doc.elements["cas:serviceResponse"]
87
- raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str}\n\nXML DOC:\n#{doc.inspect}"
87
+ raise MalformedServerResponseException, "BAD RESPONSE FROM CAS SERVER:\n#{str.inspect}\n\nXML DOC:\n#{doc.inspect}"
88
88
  end
89
89
 
90
90
  resp = doc.elements["cas:serviceResponse"].elements[1]
data/lib/cas_auth.rb CHANGED
@@ -39,20 +39,24 @@ module CAS
39
39
  # To use CAS::Filter for authentication, add something like this to
40
40
  # your environment:
41
41
  #
42
- # CAS::Filter.server_name = "yourapplication.server.name"
43
42
  # CAS::Filter.cas_base_url = "https://cas.company.com
44
43
  #
45
44
  # The filter will try to use the standard CAS page locations based on this URL.
46
45
  # Or you can explicitly specify the individual URLs:
47
46
  #
48
- # CAS::Filter.server_name = "yourapplication.server.name"
49
47
  # CAS::Filter.login_url = "https://cas.company.com/login"
50
48
  # CAS::Filter.validate_url = "https://cas.company.com/proxyValidate"
51
49
  #
50
+ # The filter will also try to automatically figure out your CAS-protected application's
51
+ # URL (to send the client back after authenticating on the CAS server), but you can
52
+ # explicitly override:
53
+ #
54
+ # CAS::Filter.service_url = "http://www.my-cas-protected-app.com/
55
+ #
52
56
  # It is of course possible to use different configurations in development, test
53
57
  # and production by placing the configuration in the appropriate environments file.
54
58
  #
55
- # To add CAS protection to a controller:
59
+ # To add CAS protection to a Rails controller:
56
60
  #
57
61
  # before_filter CAS::Filter
58
62
  #
@@ -76,25 +80,28 @@ module CAS
76
80
  @@login_url = "https://localhost/login"
77
81
  @@logout_url = nil
78
82
  @@validate_url = "https://localhost/proxyValidate"
79
- @@server_name = "localhost"
80
83
  @@renew = false
81
84
  @@session_username = :casfilteruser
82
85
  @@query_string = {}
83
86
  @@fake = nil
84
87
  @@pgt = nil
85
88
  cattr_accessor :query_string
86
- cattr_accessor :login_url, :validate_url, :service_url, :server_name, :renew, :wrap_request, :gateway, :session_username
89
+ cattr_accessor :login_url, :validate_url, :service_url, :wrap_request, :session_username
90
+ class_inheritable_accessor :gateway, :renew
87
91
  cattr_accessor :proxy_url, :proxy_callback_url, :proxy_retrieval_url
88
92
  @@authorized_proxies = []
89
93
  cattr_accessor :authorized_proxies
90
94
 
95
+ # gatewaying is disabled by default -- use GatewayFilter if you want gatewaying
96
+ self.gateway = false
91
97
 
92
98
  class << self
93
99
 
94
- # Retrieves the current Logger instance
100
+ # Retrieves the Logger used by the filter
95
101
  def logger
96
102
  CAS::LOGGER
97
103
  end
104
+ # Sets the Logger used by the filter
98
105
  def logger=(val)
99
106
  CAS::LOGGER.set_logger(val)
100
107
  end
@@ -102,6 +109,9 @@ module CAS
102
109
  alias :log :logger
103
110
  alias :log= :logger=
104
111
 
112
+ # Builds the internal logout URL. The current @@logout_url value will
113
+ # be used if it is set. Otherwise we will try to figure it out based
114
+ # on the @@login_url.
105
115
  def create_logout_url
106
116
  if !@@logout_url && @@login_url =~ %r{^(.+?)/[^/]*$}
107
117
  @@logout_url = "#{$1}/logout"
@@ -109,18 +119,28 @@ module CAS
109
119
  logger.debug "Created logout url: #{@@logout_url}"
110
120
  end
111
121
 
112
- def logout_url(controller)
122
+ # Returns the logout URL for the given controller.
123
+ # This method calls create_logout_url if no logout url has yet
124
+ # been created or set.
125
+ #
126
+ # Additionally a service URL can be provided and will be attached
127
+ # to the CAS server logout URL. If not provided, the service URL
128
+ # will be automatically derived using guess_service().
129
+ def logout_url(controller, service = nil)
113
130
  create_logout_url unless @@logout_url
114
- url = redirect_url(controller,@@logout_url)
131
+ url = redirect_url(controller,@@logout_url,service)
115
132
  logger.debug "Logout url is: #{url}"
116
133
  url
117
134
  end
118
135
 
136
+ # Explicitly sets the logout URL.
119
137
  def logout_url=(url)
120
138
  @@logout_url = url
121
139
  logger.debug "Initialized logout url to: #{url}"
122
140
  end
123
141
 
142
+ # Sets the base CAS url. The login_url, validate_url, and proxy_url
143
+ # are automagically built on top of this.
124
144
  def cas_base_url=(url)
125
145
  url.gsub!(/\/$/, '')
126
146
  CAS::Filter.login_url = "#{url}/login"
@@ -128,15 +148,28 @@ module CAS
128
148
  CAS::Filter.proxy_url = "#{url}/proxy"
129
149
  logger.debug "Initialized CAS base url to: #{url}"
130
150
  end
131
-
151
+
152
+ # Returns the current @@fake value.
153
+ # This is used for debugging. See <tt>fake=</tt> and <tt>filter_f</tt>.
132
154
  def fake
133
155
  @@fake
134
156
  end
135
157
 
158
+ # Enables or disables the fake filter.
159
+ # This is used in debugging.
160
+ #
161
+ # The argument can have one of the following values:
162
+ #
163
+ # :failure :: The fake filter will always fail.
164
+ # :param :: The fake filter will use the 'username' request param to set
165
+ # the username.
166
+ # Proc :: The fake filter will execute the given proc to determine the
167
+ # username. The current controller will be fed to the Proc as an
168
+ # argument.
169
+ # nil :: Disables the fake filter and enables the real filter.
136
170
  def fake=(val)
137
171
  if val.nil?
138
172
  alias :filter :filter_r
139
- logger.info "Will use real filter"
140
173
  else
141
174
  alias :filter :filter_f
142
175
  logger.warn "Will use fake filter"
@@ -144,9 +177,11 @@ module CAS
144
177
  @@fake = val
145
178
  end
146
179
 
180
+ # This is the fake filter method. It is aliased as 'filter'
181
+ # when the fake filter is enabled. See <tt>fake=</tt>.
147
182
  def filter_f(controller)
148
183
  logger.break
149
- logger.warn("Using fake CAS filter")
184
+ logger.warn "Using fake CAS filter"
150
185
  username = @@fake
151
186
  if :failure == @@fake
152
187
  return false
@@ -160,16 +195,36 @@ module CAS
160
195
  return true
161
196
  end
162
197
 
198
+ # This is the real filter method. It is aliased as 'filter'
199
+ # by default (when the fake filter is disabled).
200
+ #
201
+ # The filter method behaves like a standard Rails filter, taking
202
+ # the current controller as an argument (in order to access the current
203
+ # request params, the session, etc.). The method returns true
204
+ # when authentication is successful, false otherwise. Generally,
205
+ # before returning false the filter will send a HTTP redirect back to the
206
+ # CAS server.
163
207
  def filter_r(controller)
164
208
  logger.break
165
209
  logger.info("Using real CAS filter in controller: #{controller}")
166
210
 
211
+ # We store the receipt in the session so that we do not have to fetch it again
212
+ # if we're asked to validate a ticket that has already been validated. This
213
+ # saves us from unnecessarily hitting the CAS server.
167
214
  session_receipt = controller.session[:casfilterreceipt]
168
215
  session_ticket = controller.session[:caslastticket]
169
216
  ticket = controller.params[:ticket]
170
217
 
171
218
  is_valid = false
172
219
 
220
+ if controller.session[:casfiltergateway]
221
+ log.debug "Coming back from gatewayed request to CAS server..."
222
+ did_gateway = true
223
+ controller.session[:casfiltergateway] = false
224
+ else
225
+ log.debug "This request is not gatewayed."
226
+ end
227
+
173
228
  if ticket and (!session_ticket or session_ticket != ticket)
174
229
  log.info "A ticket parameter was given in the URI: #{ticket} and "+
175
230
  (!session_ticket ? "there is no previous ticket for this session" :
@@ -203,10 +258,9 @@ module CAS
203
258
  end
204
259
  end
205
260
 
206
- elsif session_receipt
261
+ elsif session_receipt && controller.session[@@session_username] && !@@renew
207
262
 
208
- log.info "Validating receipt from the session because " +
209
- (ticket ? "the given ticket #{ticket} is the same as the old ticket" : "there was no ticket given in the URI") + "."
263
+ log.info "Validating receipt from the session (instead of checking with the CAS server) because we have a :casfilteruser and the filter is not configured with @@renew."
210
264
  log.debug "The session receipt is: #{session_receipt}"
211
265
 
212
266
  is_valid = validate_receipt(session_receipt)
@@ -221,19 +275,19 @@ module CAS
221
275
 
222
276
  log.info "No ticket was given and we do not have a receipt in the session."
223
277
 
224
- did_gateway = controller.session[:casfiltergateway]
278
+
225
279
  raise CASException, "Can't redirect without login url" unless @@login_url
226
280
 
227
281
  if did_gateway
228
- if controller.session[@@session_username]
229
- log.info "We gatewayed and have a username stored in the session. The gateway was therefore successful."
230
- is_valid = true
282
+ log.info "We gatewayed and came back without authentication."
283
+ if self.gateway
284
+ log.info "This filter is configured to allow gatewaying, so we will permit the user to continue without authentication."
285
+ return true
231
286
  else
232
- log.debug "We gatewayed but do not have a username stored in the session, so we will keep session[:casfiltergateway] true"
233
- controller.session[:casfiltergateway] = true
287
+ log.warn "This filter is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
234
288
  end
235
- else
236
- log.info "We did not gateway, so we will notify the filter that the next request is being gatewayed by setting sesson[:casfiltergateway} to true"
289
+ elsif self.gateway
290
+ log.debug "We did not gateway, so we will notify the filter that the next request is being gatewayed by setting sesson[:casfiltergateway] to true"
237
291
  controller.session[:casfiltergateway] = true
238
292
  end
239
293
 
@@ -250,13 +304,18 @@ module CAS
250
304
  end
251
305
  alias :filter :filter_r
252
306
 
253
-
307
+ # Requests a proxy ticket from the CAS server and returns it as a ProxyTicketRequest object.
308
+ #
309
+ # Note that the ProxyTicketRequest object is returned regardless of whether the request
310
+ # is successful. You should check the returned object's proxy_ticket field to find out
311
+ # whether the request resulted in a valid proxy ticket.
254
312
  def request_proxy_ticket(target_service, pgt)
255
313
  r = ProxyTicketRequest.new
256
314
  r.proxy_url = @@proxy_url
257
315
  r.target_service = target_service
258
316
  r.pgt = pgt
259
317
 
318
+ # FIXME: Why is this here? The only way it would get raised is if the supplied pgt was nil/false? This might be a vestige...
260
319
  raise CAS::ProxyGrantingNotAvailable, "Cannot request a proxy ticket for service #{r.target_service} because no proxy granting ticket (PGT) has been set." unless r.pgt
261
320
 
262
321
  logger.info("Requesting proxy ticket for service: #{r.target_service} with PGT #{pgt}")
@@ -276,6 +335,10 @@ module CAS
276
335
 
277
336
  private
278
337
 
338
+ # Retrieves a proxy granting ticket corresponding to the given receipt's
339
+ # proxy granting ticket IOU from the proxy callback server.
340
+ #
341
+ # Returns a CAS::ProxyGrantingTicket object.
279
342
  def self.retrieve_pgt(receipt)
280
343
  retrieve_url = "#{@@proxy_retrieval_url}?pgtIou=#{receipt.pgt_iou}"
281
344
 
@@ -288,6 +351,7 @@ module CAS
288
351
  return pgt
289
352
  end
290
353
 
354
+ # Returns true if the given CAS::Receipt is valid; false wotherwise.
291
355
  def self.validate_receipt(receipt)
292
356
  logger.info "Checking that the receipt is valid and coherent..."
293
357
 
@@ -321,6 +385,12 @@ module CAS
321
385
  return true
322
386
  end
323
387
 
388
+ # Fetches a CAS::Receipt for the given service or proxy ticket
389
+ # and returns it.
390
+ #
391
+ # Takes the current controller as the second argument in order to
392
+ # guess the current service URL when it is not explicitly set for
393
+ # the filter.
324
394
  def self.get_receipt_for_ticket(ticket, controller)
325
395
  logger.info "Getting receipt for ticket '#{ticket}'"
326
396
  pv = ProxyTicketValidator.new
@@ -343,6 +413,11 @@ module CAS
343
413
  receipt
344
414
  end
345
415
 
416
+ # Returns the service URL for the current service.
417
+ #
418
+ # This will return the @@service_url if it has been explicitly
419
+ # set; otherwise it will try to guess the service URL based
420
+ # on the given controller parameters (see <tt>guess_service()</tt>).
346
421
  def self.service_url(controller)
347
422
  unclean = @@service_url || guess_service(controller)
348
423
  clean = remove_ticket_from_service_uri(unclean)
@@ -350,15 +425,50 @@ module CAS
350
425
  clean
351
426
  end
352
427
 
428
+ def self.server_name=(s)
429
+ puts
430
+ puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
431
+ puts "!!! CAS CONFIGURATION WARNING !!!"
432
+ puts "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"
433
+ puts
434
+ puts "CAS::Filter.server_name= no longer does anything."
435
+ puts
436
+ puts "If you want to explicitly set the service, try using:"
437
+ puts "CAS::Filter.service_url = 'http://myservice.com'"
438
+ puts
439
+ end
440
+
441
+ # Returns the URL to the login page of the CAS server with
442
+ # additional parameters like 'renew', and 'gateway' tacked
443
+ # on as appropriate. The <tt>url</tt> parameter can be used
444
+ # to use something other than the login url as the base.
445
+ #
446
+ # An optional <tt>service</tt> parameter can be provided to
447
+ # override the 'service' part of the URL.
448
+ #
353
449
  # FIXME: this method is really poorly named :(
354
- def self.redirect_url(controller,url=@@login_url)
355
- "#{url}?service=#{CGI.escape(service_url(controller))}" +
450
+ def self.redirect_url(controller,url=@@login_url,service=nil)
451
+ if service
452
+ service = remove_ticket_from_service_uri(service)
453
+ end
454
+
455
+ service = CGI.escape(service || service_url(controller))
456
+
457
+ "#{url}?service=#{service}" +
356
458
  ((@@renew)? "&renew=true":"") +
357
- ((@@gateway)? "&gateway=true":"") +
459
+ ((gateway)? "&gateway=true":"") +
358
460
  ((@@query_string.blank?)? "" : "&" +
359
461
  (@@query_string.collect { |k,v| "#{k}=#{v}"}.join("&")))
360
462
  end
361
463
 
464
+ # Tries to figure out the current service URL.
465
+ #
466
+ # This is used when the @@service_url has not been explicitly set.
467
+ # The guessed URL (generally the current URL stripped of some
468
+ # CAS-specific parameters) is fed to the CAS server so that the
469
+ # server knows where to redirect back after authentication.
470
+ #
471
+ # Also see <tt>redirect_url</tt>.
362
472
  def self.guess_service(controller)
363
473
  logger.info "Guessing service based on params: #{controller.params.inspect}"
364
474
 
@@ -378,18 +488,16 @@ module CAS
378
488
  end
379
489
 
380
490
  parms.delete("ticket")
381
-
382
- query = (parms.collect {|key, val| "#{key}=#{val}"}).join("&")
383
- query = "?" + query unless query.empty?
384
-
385
- service = "#{req.protocol}#{@@server_name}#{req.request_uri.split(/\?/)[0]}#{query}"
491
+ service = controller.url_for(parms)
386
492
 
387
493
  logger.info "Guessed service is: #{service}"
388
494
 
389
495
  return service
390
496
  end
391
497
 
498
+ # URI-encodes the
392
499
  def self.escape_service_uri(uri)
500
+ # FIXME: Why aren't we just using CGi.escape?
393
501
  URI.encode(uri, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]", false, 'U').freeze)
394
502
  end
395
503
 
@@ -401,6 +509,39 @@ module CAS
401
509
  end
402
510
  end
403
511
 
512
+ # The GatewayFilter is identical to the normal Filter, but has the gateway
513
+ # option set to true by default. This makes it easier to use in cases where
514
+ # authentication is optional.
515
+ #
516
+ # For example, say your 'index' view is accessible by authenticated and
517
+ # unauthenticated users, but you want some additional content shown for
518
+ # authenticated users. You can use the GatewayFilter to check if the user is
519
+ # already authenticated with CAS and provide them with a service ticket for
520
+ # the new service. If they are not already authenticated, then they will be
521
+ # allowed to see the 'index' view without being asked for a login.
522
+ #
523
+ # To achieve this in a Rails controller, you should set up your filters as follows:
524
+ #
525
+ # before_filter CAS::Filter, :except => [:index]
526
+ # before_filter CAS::GatewayFilter, :only => [:index]
527
+ #
528
+ # Note that you cannot use the 'renew' option with the GatewayFilter since the
529
+ # 'gateway' and 'renew' options have roughly opposite meanings -- 'renew' forces
530
+ # re-authentication, while 'gateway' makes authentication optional.
531
+ class GatewayFilter < Filter
532
+ self.gateway = true
533
+ self.renew = false
534
+
535
+ def logout_url
536
+ uri = URI.parse(super)
537
+ if uri.query?
538
+ uri.to_s + "&gateway=true"
539
+ else
540
+ uri.to_s + "?gateway=true"
541
+ end
542
+ end
543
+ end
544
+
404
545
  class ProxyGrantingNotAvailable < Exception
405
546
  end
406
547
  end
metadata CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.9.2
3
3
  specification_version: 1
4
4
  name: rubycas-client
5
5
  version: !ruby/object:Gem::Version
6
- version: 1.0.0
7
- date: 2007-07-26 00:00:00 -04:00
6
+ version: 1.1.0
7
+ date: 2007-12-21 12:37:15.306028 -05:00
8
8
  summary: Client library for the CAS single-sign-on protocol.
9
9
  require_paths:
10
10
  - lib