rubycas-client 1.0.0 → 1.1.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.
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