synapse-rubycas-server 1.1.3alpha

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