synapses-cas 0.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 (81) hide show
  1. data/CHANGELOG +3 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +27 -0
  4. data/README.md +20 -0
  5. data/Rakefile +2 -0
  6. data/bin/synapses-cas +30 -0
  7. data/config.ru +17 -0
  8. data/config/config.example.yml +592 -0
  9. data/config/unicorn.rb +88 -0
  10. data/db/migrate/001_create_initial_structure.rb +47 -0
  11. data/lib/casserver.rb +11 -0
  12. data/lib/casserver/authenticators/active_directory_ldap.rb +19 -0
  13. data/lib/casserver/authenticators/active_resource.rb +127 -0
  14. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  15. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  16. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  17. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  18. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  19. data/lib/casserver/authenticators/base.rb +67 -0
  20. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  21. data/lib/casserver/authenticators/google.rb +58 -0
  22. data/lib/casserver/authenticators/ldap.rb +147 -0
  23. data/lib/casserver/authenticators/ntlm.rb +88 -0
  24. data/lib/casserver/authenticators/open_id.rb +22 -0
  25. data/lib/casserver/authenticators/sql.rb +133 -0
  26. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  27. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  28. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  29. data/lib/casserver/authenticators/sql_rest_auth.rb +82 -0
  30. data/lib/casserver/authenticators/test.rb +22 -0
  31. data/lib/casserver/cas.rb +323 -0
  32. data/lib/casserver/localization.rb +13 -0
  33. data/lib/casserver/model.rb +270 -0
  34. data/lib/casserver/server.rb +758 -0
  35. data/lib/casserver/utils.rb +32 -0
  36. data/lib/casserver/views/_login_form.erb +42 -0
  37. data/lib/casserver/views/layout.erb +18 -0
  38. data/lib/casserver/views/login.erb +30 -0
  39. data/lib/casserver/views/proxy.builder +12 -0
  40. data/lib/casserver/views/proxy_validate.builder +25 -0
  41. data/lib/casserver/views/service_validate.builder +18 -0
  42. data/lib/casserver/views/validate.erb +2 -0
  43. data/locales/de.yml +27 -0
  44. data/locales/en.yml +26 -0
  45. data/locales/es.yml +26 -0
  46. data/locales/es_ar.yml +26 -0
  47. data/locales/fr.yml +26 -0
  48. data/locales/jp.yml +26 -0
  49. data/locales/pl.yml +26 -0
  50. data/locales/pt.yml +26 -0
  51. data/locales/ru.yml +26 -0
  52. data/locales/zh.yml +26 -0
  53. data/locales/zh_tw.yml +26 -0
  54. data/public/themes/cas.css +126 -0
  55. data/public/themes/notice.png +0 -0
  56. data/public/themes/ok.png +0 -0
  57. data/public/themes/simple/bg.png +0 -0
  58. data/public/themes/simple/favicon.png +0 -0
  59. data/public/themes/simple/login_box_bg.png +0 -0
  60. data/public/themes/simple/logo.png +0 -0
  61. data/public/themes/simple/theme.css +28 -0
  62. data/public/themes/urbacon/bg.png +0 -0
  63. data/public/themes/urbacon/login_box_bg.png +0 -0
  64. data/public/themes/urbacon/logo.png +0 -0
  65. data/public/themes/urbacon/theme.css +33 -0
  66. data/public/themes/warning.png +0 -0
  67. data/resources/init.d.sh +58 -0
  68. data/setup.rb +1585 -0
  69. data/spec/alt_config.yml +50 -0
  70. data/spec/authenticators/active_resource_spec.rb +109 -0
  71. data/spec/authenticators/ldap_spec.rb +53 -0
  72. data/spec/casserver_spec.rb +156 -0
  73. data/spec/default_config.yml +50 -0
  74. data/spec/model_spec.rb +42 -0
  75. data/spec/spec.opts +4 -0
  76. data/spec/spec_helper.rb +89 -0
  77. data/spec/utils_spec.rb +53 -0
  78. data/tasks/bundler.rake +4 -0
  79. data/tasks/db/migrate.rake +12 -0
  80. data/tasks/spec.rake +10 -0
  81. metadata +380 -0
@@ -0,0 +1,758 @@
1
+ require 'sinatra/base'
2
+ require 'casserver/localization'
3
+ require 'casserver/utils'
4
+ require 'casserver/cas'
5
+
6
+ require 'logger'
7
+ $LOG ||= Logger.new(STDOUT)
8
+
9
+ module CASServer
10
+ class Server < Sinatra::Base
11
+ if ENV['CONFIG_FILE']
12
+ CONFIG_FILE = ENV['CONFIG_FILE']
13
+ elsif !(c_file = File.dirname(__FILE__) + "/../../config.yml").nil? && File.exist?(c_file)
14
+ CONFIG_FILE = c_file
15
+ else
16
+ CONFIG_FILE = "/etc/synapses-cas/config.yml"
17
+ end
18
+
19
+ include CASServer::CAS # CAS protocol helpers
20
+ include Localization
21
+
22
+ # Use :public_folder for Sinatra >= 1.3, and :public for older versions.
23
+ def self.use_public_folder?
24
+ Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
25
+ end
26
+
27
+ set :app_file, __FILE__
28
+ set( use_public_folder? ? :public_folder : :public, # Workaround for differences in Sinatra versions.
29
+ Proc.new { settings.config[:public_dir] || File.join(root, "..", "..", "public") } )
30
+
31
+ config = HashWithIndifferentAccess.new(
32
+ :maximum_unused_login_ticket_lifetime => 5.minutes,
33
+ :maximum_unused_service_ticket_lifetime => 5.minutes, # CAS Protocol Spec, sec. 3.2.1 (recommended expiry time)
34
+ :maximum_session_lifetime => 2.days, # all tickets are deleted after this period of time
35
+ :log => {:file => 'casserver.log', :level => 'DEBUG'},
36
+ :uri_path => ""
37
+ )
38
+ set :config, config
39
+
40
+ def self.uri_path
41
+ config[:uri_path]
42
+ end
43
+
44
+ # Strip the config.uri_path from the request.path_info...
45
+ # FIXME: do we really need to override all of Sinatra's #static! to make this happen?
46
+ def static!
47
+ # Workaround for differences in Sinatra versions.
48
+ public_dir = Server.use_public_folder? ? settings.public_folder : settings.public
49
+ return if public_dir.nil?
50
+ public_dir = File.expand_path(public_dir)
51
+
52
+ path = File.expand_path(public_dir + unescape(request.path_info.gsub(/^#{settings.config[:uri_path]}/,'')))
53
+ return if path[0, public_dir.length] != public_dir
54
+ return unless File.file?(path)
55
+
56
+ env['sinatra.static_file'] = path
57
+ send_file path, :disposition => nil
58
+ end
59
+
60
+ def self.run!(options={})
61
+ set options
62
+
63
+ handler = detect_rack_handler
64
+ handler_name = handler.name.gsub(/.*::/, '')
65
+
66
+ puts "== Synapses-CAS Server is starting up " +
67
+ "on port #{config[:port] || port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
68
+
69
+ begin
70
+ opts = handler_options
71
+ rescue Exception => e
72
+ print_cli_message e, :error
73
+ raise e
74
+ end
75
+
76
+ handler.run self, opts do |server|
77
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
78
+ set :running, true
79
+ end
80
+ rescue Errno::EADDRINUSE => e
81
+ puts "== Something is already running on port #{port}!"
82
+ end
83
+
84
+ def self.quit!(server, handler_name)
85
+ ## Use thins' hard #stop! if available, otherwise just #stop
86
+ server.respond_to?(:stop!) ? server.stop! : server.stop
87
+ puts "\n== Synapses-CAS Server is shutting down" unless handler_name =~/cgi/i
88
+ end
89
+
90
+ def self.print_cli_message(msg, type = :info)
91
+ if respond_to?(:config) && config && config[:quiet]
92
+ return
93
+ end
94
+
95
+ if type == :error
96
+ io = $stderr
97
+ prefix = "!!! "
98
+ else
99
+ io = $stdout
100
+ prefix = ">>> "
101
+ end
102
+
103
+ io.puts
104
+ io.puts "#{prefix}#{msg}"
105
+ io.puts
106
+ end
107
+
108
+ def self.load_config_file(config_file)
109
+ begin
110
+ config_file = File.open(config_file)
111
+ rescue Errno::ENOENT => e
112
+
113
+ print_cli_message "Config file #{config_file} does not exist!", :error
114
+ print_cli_message "Would you like the default config file copied to #{config_file.inspect}? [y/N]"
115
+ if gets.strip.downcase == 'y'
116
+ require 'fileutils'
117
+ default_config = File.dirname(__FILE__) + '/../../config/config.example.yml'
118
+
119
+ if !File.exists?(File.dirname(config_file))
120
+ print_cli_message "Creating config directory..."
121
+ FileUtils.mkdir_p(File.dirname(config_file), :verbose => true)
122
+ end
123
+
124
+ print_cli_message "Copying #{default_config.inspect} to #{config_file.inspect}..."
125
+ FileUtils.cp(default_config, config_file, :verbose => true)
126
+ print_cli_message "The default config has been copied. You should now edit it and try starting again."
127
+ exit
128
+ else
129
+ print_cli_message "Cannot start RubyCAS-Server without a valid config file.", :error
130
+ raise e
131
+ end
132
+ rescue Errno::EACCES => e
133
+ print_cli_message "Config file #{config_file.inspect} is not readable (permission denied)!", :error
134
+ raise e
135
+ rescue => e
136
+ print_cli_message "Config file #{config_file.inspect} could not be read!", :error
137
+ raise e
138
+ end
139
+
140
+ config.merge! HashWithIndifferentAccess.new(YAML.load(config_file))
141
+ set :server, config[:server] || 'webrick'
142
+ end
143
+
144
+ def self.reconfigure!(config)
145
+ config.each do |key, val|
146
+ self.config[key] = val
147
+ end
148
+ init_database!
149
+ init_logger!
150
+ init_authenticators!
151
+ end
152
+
153
+ def self.handler_options
154
+ handler_options = {
155
+ :Host => bind || config[:bind_address],
156
+ :Port => config[:port] || 443
157
+ }
158
+
159
+ handler_options.merge(handler_ssl_options).to_hash.symbolize_keys!
160
+ end
161
+
162
+ def self.handler_ssl_options
163
+ return {} unless config[:ssl_cert]
164
+
165
+ cert_path = config[:ssl_cert]
166
+ key_path = config[:ssl_key] || config[:ssl_cert]
167
+
168
+ unless cert_path.nil? && key_path.nil?
169
+ raise "The ssl_cert and ssl_key options cannot be used with mongrel. You will have to run your " +
170
+ " server behind a reverse proxy if you want SSL under mongrel." if
171
+ config[:server] == 'mongrel'
172
+
173
+ raise "The specified certificate file #{cert_path.inspect} does not exist or is not readable. " +
174
+ " Your 'ssl_cert' configuration setting must be a path to a valid " +
175
+ " ssl certificate." unless
176
+ File.exists? cert_path
177
+
178
+ raise "The specified key file #{key_path.inspect} does not exist or is not readable. " +
179
+ " Your 'ssl_key' configuration setting must be a path to a valid " +
180
+ " ssl private key." unless
181
+ File.exists? key_path
182
+
183
+ require 'openssl'
184
+ require 'webrick/https'
185
+
186
+ cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
187
+ key = OpenSSL::PKey::RSA.new(File.read(key_path))
188
+
189
+ {
190
+ :SSLEnable => true,
191
+ :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
192
+ :SSLCertificate => cert,
193
+ :SSLPrivateKey => key
194
+ }
195
+ end
196
+ end
197
+
198
+ def self.init_authenticators!
199
+ auth = []
200
+
201
+ if config[:authenticator].nil?
202
+ print_cli_message "No authenticators have been configured. Please double-check your config file (#{CONFIG_FILE.inspect}).", :error
203
+ exit 1
204
+ end
205
+
206
+ begin
207
+ # attempt to instantiate the authenticator
208
+ config[:authenticator] = [config[:authenticator]] unless config[:authenticator].instance_of? Array
209
+ config[:authenticator].each { |authenticator| auth << authenticator[:class].constantize}
210
+ rescue NameError
211
+ if config[:authenticator].instance_of? Array
212
+ config[:authenticator].each do |authenticator|
213
+ if !authenticator[:source].nil?
214
+ # config.yml explicitly names source file
215
+ require authenticator[:source]
216
+ else
217
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
218
+ auth_rb = authenticator[:class].underscore.gsub('cas_server/', '')
219
+ require 'casserver/'+auth_rb
220
+ end
221
+ auth << authenticator[:class].constantize
222
+ end
223
+ else
224
+ if config[:authenticator][:source]
225
+ # config.yml explicitly names source file
226
+ require config[:authenticator][:source]
227
+ else
228
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
229
+ auth_rb = config[:authenticator][:class].underscore.gsub('cas_server/', '')
230
+ require 'casserver/'+auth_rb
231
+ end
232
+
233
+ auth << config[:authenticator][:class].constantize
234
+ config[:authenticator] = [config[:authenticator]]
235
+ end
236
+ end
237
+
238
+ auth.zip(config[:authenticator]).each_with_index{ |auth_conf, index|
239
+ authenticator, conf = auth_conf
240
+ $LOG.debug "About to setup #{authenticator} with #{conf.inspect}..."
241
+ authenticator.setup(conf.merge('auth_index' => index)) if authenticator.respond_to?(:setup)
242
+ $LOG.debug "Done setting up #{authenticator}."
243
+ }
244
+
245
+ set :auth, auth
246
+ end
247
+
248
+ def self.init_logger!
249
+ if config[:log]
250
+ if $LOG && config[:log][:file]
251
+ print_cli_message "Redirecting RubyCAS-Server log to #{config[:log][:file]}"
252
+ #$LOG.close
253
+ $LOG = Logger.new(config[:log][:file])
254
+ end
255
+ $LOG.level = Logger.const_get(config[:log][:level]) if config[:log][:level]
256
+ end
257
+
258
+ if config[:db_log]
259
+ if $LOG && config[:db_log][:file]
260
+ $LOG.debug "Redirecting ActiveRecord log to #{config[:log][:file]}"
261
+ #$LOG.close
262
+ ActiveRecord::Base.logger = Logger.new(config[:db_log][:file])
263
+ end
264
+ ActiveRecord::Base.logger.level = Logger.const_get(config[:db_log][:level]) if config[:db_log][:level]
265
+ end
266
+ end
267
+
268
+ def self.init_database!
269
+
270
+ unless config[:disable_auto_migrations]
271
+ ActiveRecord::Base.establish_connection(config[:database])
272
+ print_cli_message "Running migrations to make sure your database schema is up to date..."
273
+ prev_db_log = ActiveRecord::Base.logger
274
+ ActiveRecord::Base.logger = Logger.new(STDOUT)
275
+ ActiveRecord::Migration.verbose = true
276
+ ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + "/../../db/migrate")
277
+ ActiveRecord::Base.logger = prev_db_log
278
+ print_cli_message "Your database is now up to date."
279
+ end
280
+
281
+ ActiveRecord::Base.establish_connection(config[:database])
282
+ end
283
+
284
+ configure do
285
+ load_config_file(CONFIG_FILE)
286
+ init_logger!
287
+ init_database!
288
+ init_authenticators!
289
+ end
290
+
291
+ before do
292
+ content_type :html, 'charset' => 'utf-8'
293
+ @theme = settings.config[:theme]
294
+ @organization = settings.config[:organization]
295
+ @uri_path = settings.config[:uri_path]
296
+ @infoline = settings.config[:infoline]
297
+ @custom_views = settings.config[:custom_views]
298
+ @template_engine = settings.config[:template_engine] || :erb
299
+ if @template_engine != :erb
300
+ require @template_engine
301
+ @template_engine = @template_engine.to_sym
302
+ end
303
+ end
304
+
305
+ # The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
306
+ # under http://www.ja-sig.org/products/cas/overview/protocol/index.html
307
+
308
+ # 2.1 :: Login
309
+
310
+ # 2.1.1
311
+ get "#{uri_path}/login" do
312
+ CASServer::Utils::log_controller_action(self.class, params)
313
+
314
+ # make sure there's no caching
315
+ headers['Pragma'] = 'no-cache'
316
+ headers['Cache-Control'] = 'no-store'
317
+ headers['Expires'] = (Time.now - 1.year).rfc2822
318
+
319
+ # optional params
320
+ @service = clean_service_url(params['service'])
321
+ @renew = params['renew']
322
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
323
+
324
+ if tgc = request.cookies['tgt']
325
+ tgt, tgt_error = validate_ticket_granting_ticket(tgc)
326
+ end
327
+
328
+ if tgt and !tgt_error
329
+ @message = {:type => 'notice',
330
+ :message => t.notice.logged_in_as(tgt.username)}
331
+ elsif tgt_error
332
+ $LOG.debug("Ticket granting cookie could not be validated: #{tgt_error}")
333
+ elsif !tgt
334
+ $LOG.debug("No ticket granting ticket detected.")
335
+ end
336
+
337
+ if params['redirection_loop_intercepted']
338
+ @message = {:type => 'mistake',
339
+ :message => t.error.unable_to_authenticate}
340
+ end
341
+
342
+ begin
343
+ if @service
344
+ if @renew
345
+ $LOG.info("Authentication renew explicitly requested. Proceeding with CAS login for service #{@service.inspect}.")
346
+ elsif tgt && !tgt_error
347
+ $LOG.debug("Valid ticket granting ticket detected.")
348
+ st = generate_service_ticket(@service, tgt.username, tgt)
349
+ service_with_ticket = service_uri_with_ticket(@service, st)
350
+ $LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
351
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
352
+ elsif @gateway
353
+ $LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
354
+ redirect @service, 303
355
+ else
356
+ $LOG.info("Proceeding with CAS login for service #{@service.inspect}.")
357
+ end
358
+ elsif @gateway
359
+ $LOG.error("This is a gateway request but no service parameter was given!")
360
+ @message = {:type => 'mistake',
361
+ :message => t.error.no_service_parameter_given}
362
+ else
363
+ $LOG.info("Proceeding with CAS login without a target service.")
364
+ end
365
+ rescue URI::InvalidURIError
366
+ $LOG.error("The service '#{@service}' is not a valid URI!")
367
+ @message = {:type => 'mistake',
368
+ :message => t.error.invalid_target_service}
369
+ end
370
+
371
+ lt = generate_login_ticket
372
+
373
+ $LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
374
+
375
+ @lt = lt.ticket
376
+
377
+ #$LOG.debug(env)
378
+
379
+ # If the 'onlyLoginForm' parameter is specified, we will only return the
380
+ # login form part of the page. This is useful for when you want to
381
+ # embed the login form in some external page (as an IFRAME, or otherwise).
382
+ # The optional 'submitToURI' parameter can be given to explicitly set the
383
+ # action for the form, otherwise the server will try to guess this for you.
384
+ if params.has_key? 'onlyLoginForm'
385
+ if @env['HTTP_HOST']
386
+ guessed_login_uri = "http#{@env['HTTPS'] && @env['HTTPS'] == 'on' ? 's' : ''}://#{@env['REQUEST_URI']}#{self / '/login'}"
387
+ else
388
+ guessed_login_uri = nil
389
+ end
390
+
391
+ @form_action = params['submitToURI'] || guessed_login_uri
392
+
393
+ if @form_action
394
+ render :login_form
395
+ else
396
+ status 500
397
+ render t.error.invalid_submit_to_uri
398
+ end
399
+ else
400
+ render @template_engine, :login
401
+ end
402
+ end
403
+
404
+
405
+ # 2.2
406
+ post "#{uri_path}/login" do
407
+ Utils::log_controller_action(self.class, params)
408
+
409
+ # 2.2.1 (optional)
410
+ @service = clean_service_url(params['service'])
411
+
412
+ # 2.2.2 (required)
413
+ @username = params['username']
414
+ @password = params['password']
415
+ @lt = params['lt']
416
+
417
+ # Remove leading and trailing widespace from username.
418
+ @username.strip! if @username
419
+
420
+ if @username && settings.config[:downcase_username]
421
+ $LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
422
+ @username.downcase!
423
+ end
424
+
425
+ if error = validate_login_ticket(@lt)
426
+ @message = {:type => 'mistake', :message => error}
427
+ # generate another login ticket to allow for re-submitting the form
428
+ @lt = generate_login_ticket.ticket
429
+ status 500
430
+ return render @template_engine, :login
431
+ end
432
+
433
+ # generate another login ticket to allow for re-submitting the form after a post
434
+ @lt = generate_login_ticket.ticket
435
+
436
+ $LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{settings.auth.inspect}")
437
+
438
+ credentials_are_valid = false
439
+ extra_attributes = {}
440
+ successful_authenticator = nil
441
+ begin
442
+ auth_index = 0
443
+ settings.auth.each do |auth_class|
444
+ auth = auth_class.new
445
+
446
+ auth_config = settings.config[:authenticator][auth_index]
447
+ # pass the authenticator index to the configuration hash in case the authenticator needs to know
448
+ # it splace in the authenticator queue
449
+ auth.configure(auth_config.merge('auth_index' => auth_index))
450
+
451
+ credentials_are_valid = auth.validate(
452
+ :username => @username,
453
+ :password => @password,
454
+ :service => @service,
455
+ :request => @env
456
+ )
457
+ if credentials_are_valid
458
+ extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
459
+ successful_authenticator = auth
460
+ break
461
+ end
462
+
463
+ auth_index += 1
464
+ end
465
+
466
+ if credentials_are_valid
467
+ $LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
468
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
469
+
470
+ # 3.6 (ticket-granting cookie)
471
+ tgt = generate_ticket_granting_ticket(@username, extra_attributes)
472
+ response.set_cookie('tgt', tgt.to_s)
473
+
474
+ $LOG.debug("Ticket granting cookie '#{tgt.inspect}' granted to #{@username.inspect}")
475
+
476
+ if @service.blank?
477
+ $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
478
+ @message = {:type => 'confirmation', :message => t.notice.success_logged_in}
479
+ else
480
+ @st = generate_service_ticket(@service, @username, tgt)
481
+
482
+ begin
483
+ service_with_ticket = service_uri_with_ticket(@service, @st)
484
+
485
+ $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
486
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
487
+ rescue URI::InvalidURIError
488
+ $LOG.error("The service '#{@service}' is not a valid URI!")
489
+ @message = {
490
+ :type => 'mistake',
491
+ :message => t.error.invalid_target_service
492
+ }
493
+ end
494
+ end
495
+ else
496
+ $LOG.warn("Invalid credentials given for user '#{@username}'")
497
+ @message = {:type => 'mistake', :message => t.error.incorrect_username_or_password}
498
+ status 401
499
+ end
500
+ rescue CASServer::AuthenticatorError => e
501
+ $LOG.error(e)
502
+ # generate another login ticket to allow for re-submitting the form
503
+ @lt = generate_login_ticket.ticket
504
+ @message = {:type => 'mistake', :message => e.to_s}
505
+ status 401
506
+ end
507
+
508
+ render @template_engine, :login
509
+ end
510
+
511
+ get /^#{uri_path}\/?$/ do
512
+ redirect "#{config['uri_path']}/login", 303
513
+ end
514
+
515
+
516
+ # 2.3
517
+
518
+ # 2.3.1
519
+ get "#{uri_path}/logout" do
520
+ CASServer::Utils::log_controller_action(self.class, params)
521
+
522
+ # The behaviour here is somewhat non-standard. Rather than showing just a blank
523
+ # "logout" page, we take the user back to the login page with a "you have been logged out"
524
+ # message, allowing for an opportunity to immediately log back in. This makes it
525
+ # easier for the user to log out and log in as someone else.
526
+ @service = clean_service_url(params['service'] || params['destination'])
527
+ @continue_url = params['url']
528
+
529
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
530
+
531
+ tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(request.cookies['tgt'])
532
+
533
+ response.delete_cookie 'tgt'
534
+
535
+ if tgt
536
+ CASServer::Model::TicketGrantingTicket.transaction do
537
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
538
+ tgt.granted_service_tickets.each do |st|
539
+ send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
540
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
541
+ # (the above method returns false if the POST results in a non-200 HTTP response).
542
+ $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
543
+ st.destroy
544
+ end
545
+
546
+ pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
547
+ :conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
548
+ :include => :service_ticket)
549
+ pgts.each do |pgt|
550
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
551
+ pgt.destroy
552
+ end
553
+
554
+ $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
555
+ tgt.destroy
556
+ end
557
+
558
+ $LOG.info("User '#{tgt.username}' logged out.")
559
+ else
560
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
561
+ end
562
+
563
+ @message = {:type => 'confirmation', :message => t.notice.success_logged_out}
564
+
565
+ @message[:message] += t.notice.click_to_continue if @continue_url
566
+
567
+ @lt = generate_login_ticket
568
+
569
+ if @gateway && @service
570
+ redirect @service, 303
571
+ elsif @continue_url
572
+ render @template_engine, :logout
573
+ else
574
+ render @template_engine, :login
575
+ end
576
+ end
577
+
578
+
579
+ # Handler for obtaining login tickets.
580
+ # This is useful when you want to build a custom login form located on a
581
+ # remote server. Your form will have to include a valid login ticket
582
+ # value, and this can be fetched from the CAS server using the POST handler.
583
+
584
+ get "#{uri_path}/loginTicket" do
585
+ CASServer::Utils::log_controller_action(self.class, params)
586
+
587
+ $LOG.error("Tried to use login ticket dispenser with get method!")
588
+
589
+ status 422
590
+
591
+ t.error.login_ticket_needs_post_request
592
+ end
593
+
594
+
595
+ # Renders a page with a login ticket (and only the login ticket)
596
+ # in the response body.
597
+ post "#{uri_path}/loginTicket" do
598
+ CASServer::Utils::log_controller_action(self.class, params)
599
+
600
+ lt = generate_login_ticket
601
+
602
+ $LOG.debug("Dispensing login ticket #{lt} to host #{(@env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']).inspect}")
603
+
604
+ @lt = lt.ticket
605
+
606
+ @lt
607
+ end
608
+
609
+
610
+ # 2.4
611
+
612
+ # 2.4.1
613
+ get "#{uri_path}/validate" do
614
+ CASServer::Utils::log_controller_action(self.class, params)
615
+
616
+ # required
617
+ @service = clean_service_url(params['service'])
618
+ @ticket = params['ticket']
619
+ # optional
620
+ @renew = params['renew']
621
+
622
+ st, @error = validate_service_ticket(@service, @ticket)
623
+ @success = st && !@error
624
+
625
+ @username = st.username if @success
626
+
627
+ status response_status_from_error(@error) if @error
628
+
629
+ render @template_engine, :validate, :layout => false
630
+ end
631
+
632
+
633
+ # 2.5
634
+
635
+ # 2.5.1
636
+ get "#{uri_path}/serviceValidate" do
637
+ CASServer::Utils::log_controller_action(self.class, params)
638
+
639
+ # required
640
+ @service = clean_service_url(params['service'])
641
+ @ticket = params['ticket']
642
+ # optional
643
+ @pgt_url = params['pgtUrl']
644
+ @renew = params['renew']
645
+
646
+ st, @error = validate_service_ticket(@service, @ticket)
647
+ @success = st && !@error
648
+
649
+ if @success
650
+ @username = st.username
651
+ if @pgt_url
652
+ pgt = generate_proxy_granting_ticket(@pgt_url, st)
653
+ @pgtiou = pgt.iou if pgt
654
+ end
655
+ @extra_attributes = st.granted_by_tgt.extra_attributes || {}
656
+ end
657
+
658
+ status response_status_from_error(@error) if @error
659
+
660
+ render :builder, :proxy_validate
661
+ end
662
+
663
+
664
+ # 2.6
665
+
666
+ # 2.6.1
667
+ get "#{uri_path}/proxyValidate" do
668
+ CASServer::Utils::log_controller_action(self.class, params)
669
+
670
+ # required
671
+ @service = clean_service_url(params['service'])
672
+ @ticket = params['ticket']
673
+ # optional
674
+ @pgt_url = params['pgtUrl']
675
+ @renew = params['renew']
676
+
677
+ @proxies = []
678
+
679
+ t, @error = validate_proxy_ticket(@service, @ticket)
680
+ @success = t && !@error
681
+
682
+ @extra_attributes = {}
683
+ if @success
684
+ @username = t.username
685
+
686
+ if t.kind_of? CASServer::Model::ProxyTicket
687
+ @proxies << t.granted_by_pgt.service_ticket.service
688
+ end
689
+
690
+ if @pgt_url
691
+ pgt = generate_proxy_granting_ticket(@pgt_url, t)
692
+ @pgtiou = pgt.iou if pgt
693
+ end
694
+
695
+ @extra_attributes = t.granted_by_tgt.extra_attributes || {}
696
+ end
697
+
698
+ status response_status_from_error(@error) if @error
699
+
700
+ render :builder, :proxy_validate
701
+ end
702
+
703
+
704
+ # 2.7
705
+ get "#{uri_path}/proxy" do
706
+ CASServer::Utils::log_controller_action(self.class, params)
707
+
708
+ # required
709
+ @ticket = params['pgt']
710
+ @target_service = params['targetService']
711
+
712
+ pgt, @error = validate_proxy_granting_ticket(@ticket)
713
+ @success = pgt && !@error
714
+
715
+ if @success
716
+ @pt = generate_proxy_ticket(@target_service, pgt)
717
+ end
718
+
719
+ status response_status_from_error(@error) if @error
720
+
721
+ render :builder, :proxy
722
+ end
723
+
724
+
725
+
726
+ # Helpers
727
+
728
+ def response_status_from_error(error)
729
+ case error.code.to_s
730
+ when /^INVALID_/, 'BAD_PGT'
731
+ 422
732
+ when 'INTERNAL_ERROR'
733
+ 500
734
+ else
735
+ 500
736
+ end
737
+ end
738
+
739
+ def serialize_extra_attribute(builder, key, value)
740
+ if value.kind_of?(String)
741
+ builder.tag! key, value
742
+ elsif value.kind_of?(Numeric)
743
+ builder.tag! key, value.to_s
744
+ else
745
+ builder.tag! key do
746
+ builder.cdata! value.to_yaml
747
+ end
748
+ end
749
+ end
750
+
751
+ def compile_template(engine, data, options, views)
752
+ super engine, data, options, @custom_views || views
753
+ rescue Errno::ENOENT
754
+ raise unless @custom_views
755
+ super engine, data, options, views
756
+ end
757
+ end
758
+ end