synapse-rubycas-server 1.1.3alpha

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 (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