rubycas-client 1.1.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/cas_auth.rb DELETED
@@ -1,553 +0,0 @@
1
- require 'cgi'
2
- require 'logger'
3
-
4
- # these requires are needed when outside of a Rails app context (e.g. in unit tests)
5
- require 'rubygems'
6
- require 'active_support'
7
- require 'action_controller'
8
-
9
- require File.dirname(File.expand_path(__FILE__))+'/cas'
10
-
11
- module CAS
12
- # The DummyLogger is a class which might pass through to a real Logger
13
- # if one is assigned. However, it can gracefully swallow any logging calls
14
- # if there is now Logger assigned.
15
- class LoggerWrapper
16
- def initialize(logger=nil)
17
- set_logger(logger)
18
- end
19
- # Assign the 'real' Logger instance that this dummy instance wraps around.
20
- def set_logger(logger)
21
- @logger = logger
22
- end
23
- # log using the appropriate method if we have a logger
24
- # if we dont' have a logger, ignore completely.
25
- def method_missing(name, *args)
26
- if @logger && @logger.respond_to?(name)
27
- @logger.send(name, *args)
28
- end
29
- end
30
- end
31
-
32
- LOGGER = CAS::LoggerWrapper.new
33
-
34
- # Allows authentication through a CAS server.
35
- # The precondition for this filter to work is that you have an
36
- # authentication infrastructure. As such, this is for the enterprise
37
- # rather than small shops.
38
- #
39
- # To use CAS::Filter for authentication, add something like this to
40
- # your environment:
41
- #
42
- # CAS::Filter.cas_base_url = "https://cas.company.com
43
- #
44
- # The filter will try to use the standard CAS page locations based on this URL.
45
- # Or you can explicitly specify the individual URLs:
46
- #
47
- # CAS::Filter.login_url = "https://cas.company.com/login"
48
- # CAS::Filter.validate_url = "https://cas.company.com/proxyValidate"
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
- #
56
- # It is of course possible to use different configurations in development, test
57
- # and production by placing the configuration in the appropriate environments file.
58
- #
59
- # To add CAS protection to a Rails controller:
60
- #
61
- # before_filter CAS::Filter
62
- #
63
- # All of the standard Rails filter qualifiers can also be used. For example:
64
- #
65
- # before_filter CAS::Filter, :only => [:admin, :private]
66
- #
67
- # By default CAS::Filter saves the logged in user in session[:casfilteruser] but
68
- # that name can be changed by setting CAS::Filter.session_username
69
- # The username is also available from the request by
70
- #
71
- # request.username
72
- #
73
- # This wrapping of the request can be disabled by
74
- #
75
- # CAS::Filter.wrap_request = false
76
- #
77
- # Proxying is also possible. Please see the README for examples.
78
- #
79
- class Filter
80
- @@login_url = "https://localhost/login"
81
- @@logout_url = nil
82
- @@validate_url = "https://localhost/proxyValidate"
83
- @@renew = false
84
- @@session_username = :casfilteruser
85
- @@query_string = {}
86
- @@fake = nil
87
- @@pgt = nil
88
- cattr_accessor :query_string
89
- cattr_accessor :login_url, :validate_url, :service_url, :wrap_request, :session_username
90
- class_inheritable_accessor :gateway, :renew
91
- cattr_accessor :proxy_url, :proxy_callback_url, :proxy_retrieval_url
92
- @@authorized_proxies = []
93
- cattr_accessor :authorized_proxies
94
-
95
- # gatewaying is disabled by default -- use GatewayFilter if you want gatewaying
96
- self.gateway = false
97
-
98
- class << self
99
-
100
- # Retrieves the Logger used by the filter
101
- def logger
102
- CAS::LOGGER
103
- end
104
- # Sets the Logger used by the filter
105
- def logger=(val)
106
- CAS::LOGGER.set_logger(val)
107
- end
108
-
109
- alias :log :logger
110
- alias :log= :logger=
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.
115
- def create_logout_url
116
- if !@@logout_url && @@login_url =~ %r{^(.+?)/[^/]*$}
117
- @@logout_url = "#{$1}/logout"
118
- end
119
- logger.debug "Created logout url: #{@@logout_url}"
120
- end
121
-
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)
130
- create_logout_url unless @@logout_url
131
- url = redirect_url(controller,@@logout_url,service)
132
- logger.debug "Logout url is: #{url}"
133
- url
134
- end
135
-
136
- # Explicitly sets the logout URL.
137
- def logout_url=(url)
138
- @@logout_url = url
139
- logger.debug "Initialized logout url to: #{url}"
140
- end
141
-
142
- # Sets the base CAS url. The login_url, validate_url, and proxy_url
143
- # are automagically built on top of this.
144
- def cas_base_url=(url)
145
- url.gsub!(/\/$/, '')
146
- CAS::Filter.login_url = "#{url}/login"
147
- CAS::Filter.validate_url = "#{url}/proxyValidate"
148
- CAS::Filter.proxy_url = "#{url}/proxy"
149
- logger.debug "Initialized CAS base url to: #{url}"
150
- end
151
-
152
- # Returns the current @@fake value.
153
- # This is used for debugging. See <tt>fake=</tt> and <tt>filter_f</tt>.
154
- def fake
155
- @@fake
156
- end
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.
170
- def fake=(val)
171
- if val.nil?
172
- alias :filter :filter_r
173
- else
174
- alias :filter :filter_f
175
- logger.warn "Will use fake filter"
176
- end
177
- @@fake = val
178
- end
179
-
180
- # This is the fake filter method. It is aliased as 'filter'
181
- # when the fake filter is enabled. See <tt>fake=</tt>.
182
- def filter_f(controller)
183
- logger.break
184
- logger.warn "Using fake CAS filter"
185
- username = @@fake
186
- if :failure == @@fake
187
- return false
188
- elsif :param == @@fake
189
- username = controller.params['username']
190
- elsif Proc === @@fake
191
- username = @@fake.call(controller)
192
- end
193
- logger.info("The username set by the fake filter is: #{username}")
194
- controller.session[@@session_username] = username
195
- return true
196
- end
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.
207
- def filter_r(controller)
208
- logger.break
209
- logger.info("Using real CAS filter in controller: #{controller}")
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.
214
- session_receipt = controller.session[:casfilterreceipt]
215
- session_ticket = controller.session[:caslastticket]
216
- ticket = controller.params[:ticket]
217
-
218
- is_valid = false
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
-
228
- if ticket and (!session_ticket or session_ticket != ticket)
229
- log.info "A ticket parameter was given in the URI: #{ticket} and "+
230
- (!session_ticket ? "there is no previous ticket for this session" :
231
- "the ticket is different than the previous ticket, which was #{session_ticket}")
232
-
233
- receipt = get_receipt_for_ticket(ticket, controller)
234
-
235
- if receipt && validate_receipt(receipt)
236
- logger.info("Receipt for ticket request #{ticket} is valid, belongs to user #{receipt.user_name}, and will be stored in the session.")
237
- controller.session[:casfilterreceipt] = receipt
238
- controller.session[:caslastticket] = ticket
239
- controller.session[@@session_username] = receipt.user_name
240
-
241
- if receipt.pgt_iou
242
- logger.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
243
- pgt = retrieve_pgt(receipt)
244
- if pgt
245
- log.debug("Got PGT #{pgt} for PGT IOU #{receipt.pgt_iou}. This will be stored in the session.")
246
- controller.session[:casfilterpgt] = pgt
247
- else
248
- log.error("Failed to retrieve a PGT for PGT IOU #{receipt.pgt_iou}!")
249
- end
250
- end
251
-
252
- is_valid = true
253
- else
254
- if receipt
255
- log.warn "Receipt was invalid for ticket #{ticket}!"
256
- else
257
- log.warn "get_receipt_for_ticket() for ticket #{ticket} did not return a receipt!"
258
- end
259
- end
260
-
261
- elsif session_receipt && controller.session[@@session_username] && !@@renew
262
-
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."
264
- log.debug "The session receipt is: #{session_receipt}"
265
-
266
- is_valid = validate_receipt(session_receipt)
267
-
268
- if is_valid
269
- log.info "The session receipt is VALID"
270
- else
271
- log.warn "The session receipt is NOT VALID!"
272
- end
273
-
274
- else
275
-
276
- log.info "No ticket was given and we do not have a receipt in the session."
277
-
278
-
279
- raise CASException, "Can't redirect without login url" unless @@login_url
280
-
281
- if did_gateway
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
286
- else
287
- log.warn "This filter is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
288
- end
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"
291
- controller.session[:casfiltergateway] = true
292
- end
293
-
294
- end
295
-
296
- if is_valid
297
- logger.info "This request is successfully CAS authenticated for user #{controller.session[@@session_username]}!"
298
- return true
299
- else
300
- controller.session[:service] = service_url(controller)
301
- logger.info "This request is NOT CAS authenticated, so we will redirect to the login page at: #{redirect_url(controller)}"
302
- controller.send :redirect_to, redirect_url(controller) and return false
303
- end
304
- end
305
- alias :filter :filter_r
306
-
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.
312
- def request_proxy_ticket(target_service, pgt)
313
- r = ProxyTicketRequest.new
314
- r.proxy_url = @@proxy_url
315
- r.target_service = target_service
316
- r.pgt = pgt
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...
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
320
-
321
- logger.info("Requesting proxy ticket for service: #{r.target_service} with PGT #{pgt}")
322
- r.request
323
-
324
- if r.proxy_ticket
325
- logger.info("Got proxy ticket #{r.proxy_ticket} for service #{r.target_service}")
326
- else
327
- logger.warn("Did not receive a proxy ticket for service #{r.target_service}! Reason: #{r.error_code}: #{r.error_message}")
328
- end
329
-
330
- return r
331
- end
332
- end
333
-
334
-
335
-
336
- private
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.
342
- def self.retrieve_pgt(receipt)
343
- retrieve_url = "#{@@proxy_retrieval_url}?pgtIou=#{receipt.pgt_iou}"
344
-
345
- logger.debug("Will attempt to retrieve the PGT from: #{retrieve_url}")
346
-
347
- pgt = CAS::ServiceTicketValidator.retrieve(retrieve_url)
348
-
349
- logger.info("Retrieved the PGT: #{pgt}")
350
-
351
- return pgt
352
- end
353
-
354
- # Returns true if the given CAS::Receipt is valid; false wotherwise.
355
- def self.validate_receipt(receipt)
356
- logger.info "Checking that the receipt is valid and coherent..."
357
-
358
- if not receipt
359
- logger.info "No receipt given, so the receipt is invalid"
360
- return false
361
- elsif @@renew && !receipt.primary_authentication?
362
- logger.info "The filter is configured to force primary authentication (i.e. the renew options is set to true), but the receipt was not generated by primary authentication so we consider it invalid"
363
- return false
364
- end
365
-
366
- if receipt.proxied?
367
- logger.info "The receipt is proxied by proxying service: #{receipt.proxying_service}"
368
-
369
- if @@authorized_proxies and !@@authorized_proxies.empty?
370
- logger.debug "Authorized proxies are: #{@@authorized_proxies.inspect}"
371
-
372
- if !@@authorized_proxies.include? receipt.proxying_service
373
- logger.warn "Receipt was proxied by #{receipt_proxying_service} but this proxying service is not in the list of authorized proxies. The receipt is therefore invalid."
374
- return false
375
- else
376
- logger.info "Receipt is proxied by a valid proxying service."
377
- end
378
- else
379
- logger.info "No authorized proxies set, so any proxy will be considered valid"
380
- end
381
- else
382
- logger.info "Receipt is not proxied"
383
- end
384
-
385
- return true
386
- end
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.
394
- def self.get_receipt_for_ticket(ticket, controller)
395
- logger.info "Getting receipt for ticket '#{ticket}'"
396
- pv = ProxyTicketValidator.new
397
- pv.validate_url = @@validate_url
398
- pv.service_ticket = ticket
399
- pv.service = controller.session[:service] || service_url(controller)
400
- pv.renew = @@renew
401
- pv.proxy_callback_url = @@proxy_callback_url
402
- receipt = nil
403
- logger.debug "ProxyTicketValidator is: #{pv.inspect}"
404
- begin
405
- receipt = Receipt.new(pv)
406
- rescue AuthenticationException => e
407
- logger.warn("Getting a receipt for the ProxyTicketValidator threw an exception: #{e}")
408
- rescue MalformedServerResponseException => e
409
- logger.error("CAS Server returned malformed response:\n\n#{e}")
410
- raise e
411
- end
412
- logger.debug "Receipt is: #{receipt.inspect}"
413
- receipt
414
- end
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>).
421
- def self.service_url(controller)
422
- unclean = @@service_url || guess_service(controller)
423
- clean = remove_ticket_from_service_uri(unclean)
424
- logger.debug("Service URI without ticket is: #{clean}")
425
- clean
426
- end
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
- #
449
- # FIXME: this method is really poorly named :(
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}" +
458
- ((@@renew)? "&renew=true":"") +
459
- ((gateway)? "&gateway=true":"") +
460
- ((@@query_string.blank?)? "" : "&" +
461
- (@@query_string.collect { |k,v| "#{k}=#{v}"}.join("&")))
462
- end
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>.
472
- def self.guess_service(controller)
473
- logger.info "Guessing service based on params: #{controller.params.inspect}"
474
-
475
- # we're assuming that controller.params[:service] is url-encoded!
476
- if controller.params and controller.params.include? :service
477
- service = controller.params[:service]
478
- logger.info "We have a :service param, so we will URI-decode it and use this as the service: #{controller.params[:service]}"
479
- return service
480
- end
481
-
482
- req = controller.request
483
-
484
- if controller.params
485
- parms = controller.params.dup
486
- else
487
- parms = {}
488
- end
489
-
490
- parms.delete("ticket")
491
- service = controller.url_for(parms)
492
-
493
- logger.info "Guessed service is: #{service}"
494
-
495
- return service
496
- end
497
-
498
- # URI-encodes the
499
- def self.escape_service_uri(uri)
500
- # FIXME: Why aren't we just using CGi.escape?
501
- URI.encode(uri, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]", false, 'U').freeze)
502
- end
503
-
504
- # The service URI should never have a ticket parameter, but we use this to remove
505
- # any parameters named "ticket" just in case, as having a "ticket" parameter in the
506
- # service URI will generally cause an infinite redirection loop.
507
- def self.remove_ticket_from_service_uri(uri)
508
- uri.gsub(/ticket=[^&$]*&?/, '')
509
- end
510
- end
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
-
545
- class ProxyGrantingNotAvailable < Exception
546
- end
547
- end
548
-
549
- class ActionController::AbstractRequest
550
- def username
551
- session[CAS::Filter.session_username]
552
- end
553
- end