rubycas-server 0.7.1.1 → 1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. data/CHANGELOG +292 -0
  2. data/Gemfile +3 -0
  3. data/LICENSE +26 -0
  4. data/README.md +36 -0
  5. data/Rakefile +0 -3
  6. data/bin/rubycas-server +24 -19
  7. data/lib/casserver.rb +7 -110
  8. data/lib/casserver/authenticators/active_directory_ldap.rb +8 -0
  9. data/lib/casserver/authenticators/active_resource.rb +125 -0
  10. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  11. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  12. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  13. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  14. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  15. data/lib/casserver/authenticators/base.rb +30 -11
  16. data/lib/casserver/authenticators/client_certificate.rb +7 -6
  17. data/lib/casserver/authenticators/google.rb +13 -9
  18. data/lib/casserver/authenticators/ldap.rb +37 -28
  19. data/lib/casserver/authenticators/ntlm.rb +9 -9
  20. data/lib/casserver/authenticators/open_id.rb +3 -3
  21. data/lib/casserver/authenticators/sql.rb +65 -34
  22. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  23. data/lib/casserver/authenticators/sql_encrypted.rb +44 -44
  24. data/lib/casserver/authenticators/sql_md5.rb +2 -2
  25. data/lib/casserver/authenticators/sql_rest_auth.rb +82 -0
  26. data/lib/casserver/authenticators/test.rb +10 -7
  27. data/lib/casserver/cas.rb +94 -94
  28. data/lib/casserver/localization.rb +91 -0
  29. data/lib/casserver/model.rb +270 -0
  30. data/lib/casserver/server.rb +745 -0
  31. data/lib/casserver/utils.rb +9 -7
  32. data/lib/casserver/views/_login_form.erb +42 -0
  33. data/lib/casserver/views/layout.erb +18 -0
  34. data/lib/casserver/views/login.erb +30 -0
  35. data/lib/casserver/views/proxy.builder +12 -0
  36. data/lib/casserver/views/proxy_validate.builder +25 -0
  37. data/lib/casserver/views/service_validate.builder +18 -0
  38. data/lib/casserver/views/validate.erb +2 -0
  39. data/po/de_DE/rubycas-server.po +127 -0
  40. data/po/es_ES/rubycas-server.po +123 -0
  41. data/po/fr_FR/rubycas-server.po +128 -0
  42. data/po/ja_JP/rubycas-server.po +126 -0
  43. data/po/pl_PL/rubycas-server.po +123 -0
  44. data/po/pt_BR/rubycas-server.po +123 -0
  45. data/po/ru_RU/rubycas-server.po +118 -0
  46. data/po/rubycas-server.pot +112 -0
  47. data/po/zh_CN/rubycas-server.po +113 -0
  48. data/po/zh_TW/rubycas-server.po +113 -0
  49. data/public/themes/cas.css +121 -0
  50. data/{lib → public}/themes/notice.png +0 -0
  51. data/{lib → public}/themes/ok.png +0 -0
  52. data/{lib → public}/themes/simple/bg.png +0 -0
  53. data/public/themes/simple/favicon.png +0 -0
  54. data/{lib → public}/themes/simple/login_box_bg.png +0 -0
  55. data/{lib → public}/themes/simple/logo.png +0 -0
  56. data/public/themes/simple/theme.css +28 -0
  57. data/{lib → public}/themes/urbacon/bg.png +0 -0
  58. data/{lib → public}/themes/urbacon/login_box_bg.png +0 -0
  59. data/{lib → public}/themes/urbacon/logo.png +0 -0
  60. data/public/themes/urbacon/theme.css +33 -0
  61. data/{lib → public}/themes/warning.png +0 -0
  62. data/resources/init.d.sh +1 -1
  63. data/rubycas-server.gemspec +57 -0
  64. data/setup.rb +4 -4
  65. data/spec/alt_config.yml +50 -0
  66. data/spec/authenticators/active_resource_spec.rb +109 -0
  67. data/spec/authenticators/ldap_spec.rb +53 -0
  68. data/spec/casserver_spec.rb +149 -0
  69. data/spec/default_config.yml +50 -0
  70. data/spec/model_spec.rb +42 -0
  71. data/spec/spec.opts +4 -0
  72. data/spec/spec_helper.rb +88 -0
  73. data/spec/utils_spec.rb +53 -0
  74. data/tasks/bundler.rake +4 -0
  75. data/tasks/db/migrate.rake +12 -0
  76. data/tasks/localization.rake +13 -0
  77. data/tasks/spec.rake +10 -0
  78. metadata +294 -91
  79. data/CHANGELOG.txt +0 -1
  80. data/History.txt +0 -252
  81. data/LICENSE.txt +0 -504
  82. data/Manifest.txt +0 -72
  83. data/PostInstall.txt +0 -3
  84. data/README.txt +0 -25
  85. data/bin/rubycas-server-ctl +0 -22
  86. data/config.example.yml +0 -442
  87. data/config/hoe.rb +0 -76
  88. data/config/requirements.rb +0 -15
  89. data/custom_views.example.rb +0 -11
  90. data/lib/casserver/conf.rb +0 -112
  91. data/lib/casserver/controllers.rb +0 -452
  92. data/lib/casserver/environment.rb +0 -30
  93. data/lib/casserver/models.rb +0 -218
  94. data/lib/casserver/postambles.rb +0 -174
  95. data/lib/casserver/version.rb +0 -9
  96. data/lib/casserver/views.rb +0 -243
  97. data/lib/rubycas-server.rb +0 -1
  98. data/lib/rubycas-server/version.rb +0 -1
  99. data/lib/themes/cas.css +0 -121
  100. data/lib/themes/simple/theme.css +0 -28
  101. data/lib/themes/urbacon/theme.css +0 -33
  102. data/misc/basic_cas_single_signon_mechanism_diagram.png +0 -0
  103. data/misc/basic_cas_single_signon_mechanism_diagram.svg +0 -652
  104. data/script/console +0 -10
  105. data/script/destroy +0 -14
  106. data/script/generate +0 -14
  107. data/script/txt2html +0 -82
  108. data/tasks/deployment.rake +0 -34
  109. data/tasks/environment.rake +0 -7
  110. data/tasks/website.rake +0 -17
  111. data/vendor/isaac_0.9.1/LICENSE +0 -26
  112. data/vendor/isaac_0.9.1/README +0 -78
  113. data/vendor/isaac_0.9.1/TODO +0 -3
  114. data/vendor/isaac_0.9.1/VERSIONS +0 -3
  115. data/vendor/isaac_0.9.1/crypt/ISAAC.rb +0 -171
  116. data/vendor/isaac_0.9.1/isaac.gemspec +0 -39
  117. data/vendor/isaac_0.9.1/setup.rb +0 -596
  118. data/vendor/isaac_0.9.1/test/TC_ISAAC.rb +0 -76
  119. data/website/index.html +0 -40
  120. data/website/index.txt +0 -3
  121. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  122. data/website/stylesheets/screen.css +0 -138
  123. data/website/template.html.erb +0 -40
@@ -0,0 +1,93 @@
1
+ require 'casserver/authenticators/sql'
2
+
3
+ # These were pulled directly from Authlogic, and new ones can be added
4
+ # just by including new Crypto Providers
5
+ require File.dirname(__FILE__) + '/authlogic_crypto_providers/aes256'
6
+ require File.dirname(__FILE__) + '/authlogic_crypto_providers/bcrypt'
7
+ require File.dirname(__FILE__) + '/authlogic_crypto_providers/md5'
8
+ require File.dirname(__FILE__) + '/authlogic_crypto_providers/sha1'
9
+ require File.dirname(__FILE__) + '/authlogic_crypto_providers/sha512'
10
+
11
+ begin
12
+ require 'active_record'
13
+ rescue LoadError
14
+ require 'rubygems'
15
+ require 'active_record'
16
+ end
17
+
18
+ # This is a version of the SQL authenticator that works nicely with Authlogic.
19
+ # Passwords are encrypted the same way as it done in Authlogic.
20
+ # Before use you this, you MUST configure rest_auth_digest_streches and rest_auth_site_key in
21
+ # config.
22
+ #
23
+ # Using this authenticator requires restful authentication plugin on rails (client) side.
24
+ #
25
+ # * git://github.com/binarylogic/authlogic.git
26
+ #
27
+ # Usage:
28
+
29
+ # authenticator:
30
+ # class: CASServer::Authenticators::SQLAuthlogic
31
+ # database:
32
+ # adapter: mysql
33
+ # database: some_database_with_users_table
34
+ # user: root
35
+ # password:
36
+ # server: localhost
37
+ # user_table: user
38
+ # username_column: login
39
+ # password_column: crypted_password
40
+ # salt_column: password_salt
41
+ # encryptor: Sha1
42
+ # encryptor_options:
43
+ # digest_format: --SALT--PASSWORD--
44
+ # stretches: 1
45
+ #
46
+ class CASServer::Authenticators::SQLAuthlogic < CASServer::Authenticators::SQL
47
+
48
+ def validate(credentials)
49
+ read_standard_credentials(credentials)
50
+ raise_if_not_configured
51
+
52
+ user_model = self.class.user_model
53
+
54
+ username_column = @options[:username_column] || "login"
55
+ password_column = @options[:password_column] || "crypted_password"
56
+ salt_column = @options[:salt_column]
57
+
58
+ $LOG.debug "#{self.class}: [#{user_model}] " + "Connection pool size: #{user_model.connection_pool.instance_variable_get(:@checked_out).length}/#{user_model.connection_pool.instance_variable_get(:@connections).length}"
59
+ results = user_model.find(:all, :conditions => ["#{username_column} = ?", @username])
60
+ user_model.connection_pool.checkin(user_model.connection)
61
+
62
+ begin
63
+ encryptor = eval("Authlogic::CryptoProviders::" + @options[:encryptor] || "Sha512")
64
+ rescue
65
+ $LOG.warn("Could not initialize Authlogic crypto class for '#{@options[:encryptor]}'")
66
+ encryptor = Authlogic::CryptoProviders::Sha512
67
+ end
68
+
69
+ @options[:encryptor_options].each do |name, value|
70
+ encryptor.send("#{name}=", value) if encryptor.respond_to?("#{name}=")
71
+ end
72
+
73
+ if results.size > 0
74
+ $LOG.warn("Multiple matches found for user '#{@username}'") if results.size > 1
75
+ user = results.first
76
+ tokens = [@password, (not salt_column.nil?) && user.send(salt_column) || nil].compact
77
+ crypted = user.send(password_column)
78
+
79
+ unless @options[:extra_attributes].blank?
80
+ if results.size > 1
81
+ $LOG.warn("#{self.class}: Unable to extract extra_attributes because multiple matches were found for #{@username.inspect}")
82
+ else
83
+ extract_extra(user)
84
+ log_extra
85
+ end
86
+ end
87
+
88
+ return encryptor.matches?(crypted, tokens)
89
+ else
90
+ return false
91
+ end
92
+ end
93
+ end
@@ -1,50 +1,18 @@
1
- require 'casserver/authenticators/base'
1
+ require 'casserver/authenticators/sql'
2
2
 
3
3
  require 'digest/sha1'
4
4
  require 'digest/sha2'
5
+ require 'crypt-isaac'
5
6
 
6
- $: << File.dirname(File.expand_path(__FILE__)) + "/../../../vendor/isaac_0.9.1"
7
- require 'crypt/ISAAC'
8
-
9
- begin
10
- require 'active_record'
11
- rescue LoadError
12
- require 'rubygems'
13
- require 'active_record'
14
- end
15
-
16
- # This is a more secure version of the SQL authenticator. Passwords are encrypted
7
+ # This is a more secure version of the SQL authenticator. Passwords are encrypted
17
8
  # rather than being stored in plain text.
18
9
  #
19
10
  # Based on code contributed by Ben Mabey.
20
11
  #
21
12
  # Using this authenticator requires some configuration on the client side. Please see
22
13
  # http://code.google.com/p/rubycas-server/wiki/UsingTheSQLEncryptedAuthenticator
23
- class CASServer::Authenticators::SQLEncrypted < CASServer::Authenticators::Base
24
-
25
- def validate(credentials)
26
- read_standard_credentials(credentials)
27
-
28
- raise CASServer::AuthenticatorError, "Cannot validate credentials because the authenticator hasn't yet been configured" unless @options
29
- raise CASServer::AuthenticatorError, "Invalid authenticator configuration!" unless @options[:database]
30
-
31
- CASUser.establish_connection @options[:database]
32
- CASUser.set_table_name @options[:user_table] || "users"
33
-
34
- username_column = @options[:username_column] || "username"
35
-
36
- results = CASUser.find(:all, :conditions => ["#{username_column} = ?", @username])
37
-
38
- if results.size > 0
39
- $LOG.warn("Multiple matches found for user '#{@username}'") if results.size > 1
40
- user = results.first
41
- return user.encrypted_password == user.encrypt(@password)
42
- else
43
- return false
44
- end
45
- end
46
-
47
- # Include this module into your application's user model.
14
+ class CASServer::Authenticators::SQLEncrypted < CASServer::Authenticators::SQL
15
+ # Include this module into your application's user model.
48
16
  #
49
17
  # Your model must have an 'encrypted_password' column where the password will be stored,
50
18
  # and an 'encryption_salt' column that will be populated with a random string before
@@ -54,22 +22,54 @@ class CASServer::Authenticators::SQLEncrypted < CASServer::Authenticators::Base
54
22
  raise "#{self} should be inclued in an ActiveRecord class!" unless mod.respond_to?(:before_save)
55
23
  mod.before_save :generate_encryption_salt
56
24
  end
57
-
25
+
58
26
  def encrypt(str)
27
+ generate_encryption_salt unless encryption_salt
59
28
  Digest::SHA256.hexdigest("#{encryption_salt}::#{str}")
60
29
  end
61
-
30
+
62
31
  def password=(password)
63
32
  self[:encrypted_password] = encrypt(password)
64
33
  end
65
-
34
+
66
35
  def generate_encryption_salt
67
36
  self.encryption_salt = Digest::SHA1.hexdigest(Crypt::ISAAC.new.rand(2**31).to_s) unless
68
37
  encryption_salt
69
38
  end
70
39
  end
71
-
72
- class CASUser < ActiveRecord::Base
73
- include EncryptedPassword
40
+
41
+ def self.setup(options)
42
+ super(options)
43
+ user_model.__send__(:include, EncryptedPassword)
74
44
  end
75
- end
45
+
46
+ def validate(credentials)
47
+ read_standard_credentials(credentials)
48
+ raise_if_not_configured
49
+
50
+ user_model = self.class.user_model
51
+
52
+ username_column = @options[:username_column] || "username"
53
+ encrypt_function = @options[:encrypt_function] || 'user.encrypted_password == Digest::SHA256.hexdigest("#{user.encryption_salt}::#{@password}")'
54
+
55
+ $LOG.debug "#{self.class}: [#{user_model}] " + "Connection pool size: #{user_model.connection_pool.instance_variable_get(:@checked_out).length}/#{user_model.connection_pool.instance_variable_get(:@connections).length}"
56
+ results = user_model.find(:all, :conditions => ["#{username_column} = ?", @username])
57
+ user_model.connection_pool.checkin(user_model.connection)
58
+
59
+ if results.size > 0
60
+ $LOG.warn("Multiple matches found for user '#{@username}'") if results.size > 1
61
+ user = results.first
62
+ unless @options[:extra_attributes].blank?
63
+ if results.size > 1
64
+ $LOG.warn("#{self.class}: Unable to extract extra_attributes because multiple matches were found for #{@username.inspect}")
65
+ else
66
+ extract_extra(user)
67
+ log_extra
68
+ end
69
+ end
70
+ return eval(encrypt_function)
71
+ else
72
+ return false
73
+ end
74
+ end
75
+ end
@@ -9,11 +9,11 @@ require 'digest/md5'
9
9
  # Drupal, you should use 'name' for the :username_column config option, and
10
10
  # 'pass' for the :password_column.
11
11
  class CASServer::Authenticators::SQLMd5 < CASServer::Authenticators::SQL
12
-
12
+
13
13
  protected
14
14
  def read_standard_credentials(credentials)
15
15
  super
16
16
  @password = Digest::MD5.hexdigest(@password)
17
17
  end
18
18
 
19
- end
19
+ end
@@ -0,0 +1,82 @@
1
+ require 'casserver/authenticators/sql_encrypted'
2
+
3
+ require 'digest/sha1'
4
+
5
+ begin
6
+ require 'active_record'
7
+ rescue LoadError
8
+ require 'rubygems'
9
+ require 'active_record'
10
+ end
11
+
12
+ # This is a version of the SQL authenticator that works nicely with RestfulAuthentication.
13
+ # Passwords are encrypted the same way as it done in RestfulAuthentication.
14
+ # Before use you this, you MUST configure rest_auth_digest_streches and rest_auth_site_key in
15
+ # config.
16
+ #
17
+ # Using this authenticator requires restful authentication plugin on rails (client) side.
18
+ #
19
+ # * git://github.com/technoweenie/restful-authentication.git
20
+ #
21
+ class CASServer::Authenticators::SQLRestAuth < CASServer::Authenticators::SQLEncrypted
22
+
23
+ def validate(credentials)
24
+ read_standard_credentials(credentials)
25
+ raise_if_not_configured
26
+
27
+ raise CASServer::AuthenticatorError, "You must specify a 'site_key' in the SQLRestAuth authenticator's configuration!" unless @options[:site_key]
28
+ raise CASServer::AuthenticatorError, "You must specify 'digest_streches' in the SQLRestAuth authenticator's configuration!" unless @options[:digest_streches]
29
+
30
+ user_model = self.class.user_model
31
+
32
+ username_column = @options[:username_column] || "email"
33
+
34
+ $LOG.debug "#{self.class}: [#{user_model}] " + "Connection pool size: #{user_model.connection_pool.instance_variable_get(:@checked_out).length}/#{user_model.connection_pool.instance_variable_get(:@connections).length}"
35
+ results = user_model.find(:all, :conditions => ["#{username_column} = ?", @username])
36
+ user_model.connection_pool.checkin(user_model.connection)
37
+
38
+ if results.size > 0
39
+ $LOG.warn("Multiple matches found for user '#{@username}'") if results.size > 1
40
+ user = results.first
41
+ if user.crypted_password == user.encrypt(@password)
42
+ unless @options[:extra_attributes].blank?
43
+ extract_extra(user)
44
+ log_extra
45
+ end
46
+ return true
47
+ else
48
+ return false
49
+ end
50
+ else
51
+ return false
52
+ end
53
+ end
54
+
55
+ def self.setup(options)
56
+ super(options)
57
+ user_model.__send__(:include, EncryptedPassword)
58
+ end
59
+
60
+ module EncryptedPassword
61
+
62
+ def self.included(mod)
63
+ raise "#{self} should be inclued in an ActiveRecord class!" unless mod.respond_to?(:before_save)
64
+ end
65
+
66
+ def encrypt(password)
67
+ password_digest(password, self.salt)
68
+ end
69
+
70
+ def secure_digest(*args)
71
+ Digest::SHA1.hexdigest(args.flatten.join('--'))
72
+ end
73
+
74
+ def password_digest(password, salt)
75
+ digest = @options[:site_key]
76
+ @options[:digest_streches].times do
77
+ digest = secure_digest(digest, salt, password, @options[:site_key])
78
+ end
79
+ digest
80
+ end
81
+ end
82
+ end
@@ -1,19 +1,22 @@
1
+ # encoding: UTF-8
1
2
  require 'casserver/authenticators/base'
2
3
 
3
- # Dummy authenticator used for testing.
4
- # Accepts "testuser" for username and "testpassword" for password; otherwise authentication fails.
4
+ # Dummy authenticator used for testing.
5
+ # Accepts any username as valid as long as the password is "testpassword"; otherwise authentication fails.
5
6
  # Raises an AuthenticationError when username is "do_error" (this is useful to test the Exception
6
7
  # handling functionality).
7
8
  class CASServer::Authenticators::Test < CASServer::Authenticators::Base
8
9
  def validate(credentials)
9
10
  read_standard_credentials(credentials)
10
-
11
+
11
12
  raise CASServer::AuthenticatorError, "Username is 'do_error'!" if @username == 'do_error'
12
-
13
- @extra_attributes[:test_string] = "testing!"
13
+
14
+ @extra_attributes[:test_utf_string] = "Ютф"
14
15
  @extra_attributes[:test_numeric] = 123.45
15
16
  @extra_attributes[:test_serialized] = {:foo => 'bar', :alpha => [1,2,3]}
16
-
17
- return @password == "testpassword"
17
+
18
+ valid_password = options[:password] || "testpassword"
19
+
20
+ return @password == valid_password
18
21
  end
19
22
  end
@@ -1,28 +1,31 @@
1
1
  require 'uri'
2
2
  require 'net/https'
3
3
 
4
+ require 'casserver/model'
5
+
4
6
  # Encapsulates CAS functionality. This module is meant to be included in
5
7
  # the CASServer::Controllers module.
6
8
  module CASServer::CAS
7
9
 
8
- include CASServer::Models
10
+ include CASServer::Model
9
11
 
10
12
  def generate_login_ticket
11
13
  # 3.5 (login ticket)
12
14
  lt = LoginTicket.new
13
15
  lt.ticket = "LT-" + CASServer::Utils.random_string
14
- lt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
16
+
17
+ lt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
15
18
  lt.save!
16
19
  $LOG.debug("Generated login ticket '#{lt.ticket}' for client" +
17
20
  " at '#{lt.client_hostname}'")
18
21
  lt
19
22
  end
20
-
23
+
21
24
  # Creates a TicketGrantingTicket for the given username. This is done when the user logs in
22
25
  # for the first time to establish their SSO session (after their credentials have been validated).
23
26
  #
24
27
  # The optional 'extra_attributes' parameter takes a hash of additional attributes
25
- # that will be sent along with the username in the CAS response to subsequent
28
+ # that will be sent along with the username in the CAS response to subsequent
26
29
  # validation requests from clients.
27
30
  def generate_ticket_granting_ticket(username, extra_attributes = {})
28
31
  # 3.6 (ticket granting cookie/ticket)
@@ -30,152 +33,156 @@ module CASServer::CAS
30
33
  tgt.ticket = "TGC-" + CASServer::Utils.random_string
31
34
  tgt.username = username
32
35
  tgt.extra_attributes = extra_attributes
33
- tgt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
36
+ tgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
34
37
  tgt.save!
35
38
  $LOG.debug("Generated ticket granting ticket '#{tgt.ticket}' for user" +
36
- " '#{tgt.username}' at '#{tgt.client_hostname}'" +
39
+ " '#{tgt.username}' at '#{tgt.client_hostname}'" +
37
40
  (extra_attributes.blank? ? "" : " with extra attributes #{extra_attributes.inspect}"))
38
41
  tgt
39
42
  end
40
-
43
+
41
44
  def generate_service_ticket(service, username, tgt)
42
45
  # 3.1 (service ticket)
43
46
  st = ServiceTicket.new
44
47
  st.ticket = "ST-" + CASServer::Utils.random_string
45
48
  st.service = service
46
49
  st.username = username
47
- st.ticket_granting_ticket = tgt
48
- st.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
50
+ st.granted_by_tgt_id = tgt.id
51
+ st.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
49
52
  st.save!
50
53
  $LOG.debug("Generated service ticket '#{st.ticket}' for service '#{st.service}'" +
51
54
  " for user '#{st.username}' at '#{st.client_hostname}'")
52
55
  st
53
56
  end
54
-
57
+
55
58
  def generate_proxy_ticket(target_service, pgt)
56
59
  # 3.2 (proxy ticket)
57
60
  pt = ProxyTicket.new
58
61
  pt.ticket = "PT-" + CASServer::Utils.random_string
59
62
  pt.service = target_service
60
63
  pt.username = pgt.service_ticket.username
61
- pt.proxy_granting_ticket_id = pgt.id
62
- pt.ticket_granting_ticket = pgt.service_ticket.ticket_granting_ticket
63
- pt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
64
+ pt.granted_by_pgt_id = pgt.id
65
+ pt.granted_by_tgt_id = pgt.service_ticket.granted_by_tgt.id
66
+ pt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
64
67
  pt.save!
65
68
  $LOG.debug("Generated proxy ticket '#{pt.ticket}' for target service '#{pt.service}'" +
66
69
  " for user '#{pt.username}' at '#{pt.client_hostname}' using proxy-granting" +
67
70
  " ticket '#{pgt.ticket}'")
68
71
  pt
69
72
  end
70
-
73
+
71
74
  def generate_proxy_granting_ticket(pgt_url, st)
72
75
  uri = URI.parse(pgt_url)
73
76
  https = Net::HTTP.new(uri.host,uri.port)
74
77
  https.use_ssl = true
75
-
78
+
76
79
  # Here's what's going on here:
77
- #
80
+ #
78
81
  # 1. We generate a ProxyGrantingTicket (but don't store it in the database just yet)
79
82
  # 2. Deposit the PGT and it's associated IOU at the proxy callback URL.
80
83
  # 3. If the proxy callback URL responds with HTTP code 200, store the PGT and return it;
81
84
  # otherwise don't save it and return nothing.
82
- #
85
+ #
83
86
  https.start do |conn|
84
87
  path = uri.path.empty? ? '/' : uri.path
88
+ path += '?' + uri.query unless (uri.query.nil? || uri.query.empty?)
85
89
 
86
90
  pgt = ProxyGrantingTicket.new
87
91
  pgt.ticket = "PGT-" + CASServer::Utils.random_string(60)
88
92
  pgt.iou = "PGTIOU-" + CASServer::Utils.random_string(57)
89
93
  pgt.service_ticket_id = st.id
90
- pgt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
91
-
94
+ pgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
95
+
92
96
  # FIXME: The CAS protocol spec says to use 'pgt' as the parameter, but in practice
93
97
  # the JA-SIG and Yale server implementations use pgtId. We'll go with the
94
98
  # in-practice standard.
95
99
  path += (uri.query.nil? || uri.query.empty? ? '?' : '&') + "pgtId=#{pgt.ticket}&pgtIou=#{pgt.iou}"
96
-
100
+
97
101
  response = conn.request_get(path)
98
102
  # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
103
+ # NOTE: The following response codes are valid according to the JA-SIG implementation even without following redirects
99
104
 
100
- if response.code.to_i == 200
105
+ if %w(200 202 301 302 304).include?(response.code)
101
106
  # 3.4 (proxy-granting ticket IOU)
102
107
  pgt.save!
103
108
  $LOG.debug "PGT generated for pgt_url '#{pgt_url}': #{pgt.inspect}"
104
109
  pgt
105
110
  else
106
111
  $LOG.warn "PGT callback server responded with a bad result code '#{response.code}'. PGT will not be stored."
112
+ nil
107
113
  end
108
114
  end
109
115
  end
110
-
116
+
111
117
  def validate_login_ticket(ticket)
112
118
  $LOG.debug("Validating login ticket '#{ticket}'")
113
-
119
+
114
120
  success = false
115
121
  if ticket.nil?
116
- error = "Your login request did not include a login ticket. There may be a problem with the authentication system."
117
- $LOG.warn("Missing login ticket.")
122
+ error = _("Your login request did not include a login ticket. There may be a problem with the authentication system.")
123
+ $LOG.warn "Missing login ticket."
118
124
  elsif lt = LoginTicket.find_by_ticket(ticket)
119
125
  if lt.consumed?
120
- error = "The login ticket you provided has already been used up. Please try logging in again."
121
- $LOG.warn("Login ticket '#{ticket}' previously used up")
122
- elsif Time.now - lt.created_on < CASServer::Conf.login_ticket_expiry
123
- $LOG.info("Login ticket '#{ticket}' successfully validated")
126
+ error = _("The login ticket you provided has already been used up. Please try logging in again.")
127
+ $LOG.warn "Login ticket '#{ticket}' previously used up"
128
+ elsif Time.now - lt.created_on < settings.config[:maximum_unused_login_ticket_lifetime]
129
+ $LOG.info "Login ticket '#{ticket}' successfully validated"
124
130
  else
125
- error = "Your login ticket has expired. Please try logging in again."
126
- $LOG.warn("Expired login ticket '#{ticket}'")
131
+ error = _("You took too long to enter your credentials. Please try again.")
132
+ $LOG.warn "Expired login ticket '#{ticket}'"
127
133
  end
128
134
  else
129
- error = "The login ticket you provided is invalid. Please try logging in again."
130
- $LOG.warn("Invalid login ticket '#{ticket}'")
135
+ error = _("The login ticket you provided is invalid. There may be a problem with the authentication system.")
136
+ $LOG.warn "Invalid login ticket '#{ticket}'"
131
137
  end
132
-
138
+
133
139
  lt.consume! if lt
134
-
140
+
135
141
  error
136
142
  end
137
-
143
+
138
144
  def validate_ticket_granting_ticket(ticket)
139
145
  $LOG.debug("Validating ticket granting ticket '#{ticket}'")
140
-
146
+
141
147
  if ticket.nil?
142
148
  error = "No ticket granting ticket given."
143
- $LOG.debug(error)
149
+ $LOG.debug error
144
150
  elsif tgt = TicketGrantingTicket.find_by_ticket(ticket)
145
- if CASServer::Conf.expire_sessions && Time.now - tgt.created_on > CASServer::Conf.ticket_granting_ticket_expiry
151
+ if settings.config[:maximum_session_lifetime] && Time.now - tgt.created_on > settings.config[:maximum_session_lifetime]
152
+ tgt.destroy
146
153
  error = "Your session has expired. Please log in again."
147
- $LOG.info("Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired.")
154
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired."
148
155
  else
149
- $LOG.info("Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated.")
156
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated."
150
157
  end
151
158
  else
152
159
  error = "Invalid ticket granting ticket '#{ticket}' (no matching ticket found in the database)."
153
160
  $LOG.warn(error)
154
161
  end
155
-
162
+
156
163
  [tgt, error]
157
164
  end
158
165
 
159
166
  def validate_service_ticket(service, ticket, allow_proxy_tickets = false)
160
- $LOG.debug("Validating service/proxy ticket '#{ticket}' for service '#{service}'")
161
-
167
+ $LOG.debug "Validating service/proxy ticket '#{ticket}' for service '#{service}'"
168
+
162
169
  if service.nil? or ticket.nil?
163
170
  error = Error.new(:INVALID_REQUEST, "Ticket or service parameter was missing in the request.")
164
- $LOG.warn("#{error.code} - #{error.message}")
171
+ $LOG.warn "#{error.code} - #{error.message}"
165
172
  elsif st = ServiceTicket.find_by_ticket(ticket)
166
173
  if st.consumed?
167
174
  error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has already been used up.")
168
- $LOG.warn("#{error.code} - #{error.message}")
169
- elsif st.kind_of?(CASServer::Models::ProxyTicket) && !allow_proxy_tickets
175
+ $LOG.warn "#{error.code} - #{error.message}"
176
+ elsif st.kind_of?(CASServer::Model::ProxyTicket) && !allow_proxy_tickets
170
177
  error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' is a proxy ticket, but only service tickets are allowed here.")
171
- $LOG.warn("#{error.code} - #{error.message}")
172
- elsif Time.now - st.created_on > CASServer::Conf.service_ticket_expiry
178
+ $LOG.warn "#{error.code} - #{error.message}"
179
+ elsif Time.now - st.created_on > settings.config[:maximum_unused_service_ticket_lifetime]
173
180
  error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has expired.")
174
- $LOG.warn("Ticket '#{ticket}' has expired.")
181
+ $LOG.warn "Ticket '#{ticket}' has expired."
175
182
  elsif !st.matches_service? service
176
183
  error = Error.new(:INVALID_SERVICE, "The ticket '#{ticket}' belonging to user '#{st.username}' is valid,"+
177
184
  " but the requested service '#{service}' does not match the service '#{st.service}' associated with this ticket.")
178
- $LOG.warn("#{error.code} - #{error.message}")
185
+ $LOG.warn "#{error.code} - #{error.message}"
179
186
  else
180
187
  $LOG.info("Ticket '#{ticket}' for service '#{service}' for user '#{st.username}' successfully validated.")
181
188
  end
@@ -183,30 +190,30 @@ module CASServer::CAS
183
190
  error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' not recognized.")
184
191
  $LOG.warn("#{error.code} - #{error.message}")
185
192
  end
186
-
193
+
187
194
  if st
188
195
  st.consume!
189
196
  end
190
-
191
-
197
+
198
+
192
199
  [st, error]
193
200
  end
194
-
201
+
195
202
  def validate_proxy_ticket(service, ticket)
196
203
  pt, error = validate_service_ticket(service, ticket, true)
197
-
198
- if pt.kind_of?(CASServer::Models::ProxyTicket) && !error
199
- if not pt.proxy_granting_ticket
204
+
205
+ if pt.kind_of?(CASServer::Model::ProxyTicket) && !error
206
+ if not pt.granted_by_pgt
200
207
  error = Error.new(:INTERNAL_ERROR, "Proxy ticket '#{pt}' belonging to user '#{pt.username}' is not associated with a proxy granting ticket.")
201
- elsif not pt.proxy_granting_ticket.service_ticket
202
- error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{pt.proxy_granting_ticket}'"+
208
+ elsif not pt.granted_by_pgt.service_ticket
209
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{pt.granted_by_pgt}'"+
203
210
  " (associated with proxy ticket '#{pt}' and belonging to user '#{pt.username}' is not associated with a service ticket.")
204
211
  end
205
212
  end
206
-
213
+
207
214
  [pt, error]
208
215
  end
209
-
216
+
210
217
  def validate_proxy_granting_ticket(ticket)
211
218
  if ticket.nil?
212
219
  error = Error.new(:INVALID_REQUEST, "pgt parameter was missing in the request.")
@@ -222,10 +229,10 @@ module CASServer::CAS
222
229
  error = Error.new(:BAD_PGT, "Invalid proxy granting ticket '#{ticket}' (no matching ticket found in the database).")
223
230
  $LOG.warn("#{error.code} - #{error.message}")
224
231
  end
225
-
232
+
226
233
  [pgt, error]
227
234
  end
228
-
235
+
229
236
  # Takes an existing ServiceTicket object (presumably pulled from the database)
230
237
  # and sends a POST with logout information to the service that the ticket
231
238
  # was generated for.
@@ -234,26 +241,15 @@ module CASServer::CAS
234
241
  # See http://www.ja-sig.org/wiki/display/CASUM/Single+Sign+Out
235
242
  def send_logout_notification_for_service_ticket(st)
236
243
  uri = URI.parse(st.service)
237
- http = Net::HTTP.new(uri.host, uri.port)
238
- #http.use_ssl = true if uri.scheme = 'https'
239
-
244
+ uri.path = '/' if uri.path.empty?
240
245
  time = Time.now
241
246
  rand = CASServer::Utils.random_string
242
-
243
- path = uri.path
244
- path = '/' if path.empty?
245
-
246
- req = Net::HTTP::Post.new(path)
247
- req.set_form_data(
248
- 'logoutRequest' => %{<samlp:LogoutRequest ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
249
- <saml:NameID></saml:NameID>
250
- <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
251
- </samlp:LogoutRequest>}
252
- )
253
-
254
- http.start do |conn|
255
- response = conn.request(req)
256
-
247
+
248
+ begin
249
+ response = Net::HTTP.post_form(uri, {'logoutRequest' => URI.escape(%{<samlp:LogoutRequest ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
250
+ <saml:NameID></saml:NameID>
251
+ <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
252
+ </samlp:LogoutRequest>})})
257
253
  if response.kind_of? Net::HTTPSuccess
258
254
  $LOG.info "Logout notification successfully posted to #{st.service.inspect}."
259
255
  return true
@@ -261,16 +257,19 @@ module CASServer::CAS
261
257
  $LOG.error "Service #{st.service.inspect} responed to logout notification with code '#{response.code}'!"
262
258
  return false
263
259
  end
260
+ rescue Exception => e
261
+ $LOG.error "Failed to send logout notification to service #{st.service.inspect} due to #{e}"
262
+ return false
264
263
  end
265
264
  end
266
-
265
+
267
266
  def service_uri_with_ticket(service, st)
268
- raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? CASServer::Models::ServiceTicket
269
-
267
+ raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? CASServer::Model::ServiceTicket
268
+
270
269
  # This will choke with a URI::InvalidURIError if service URI is not properly URI-escaped...
271
270
  # This exception is handled further upstream (i.e. in the controller).
272
271
  service_uri = URI.parse(service)
273
-
272
+
274
273
  if service.include? "?"
275
274
  if service_uri.query.empty?
276
275
  query_separator = ""
@@ -280,11 +279,11 @@ module CASServer::CAS
280
279
  else
281
280
  query_separator = "?"
282
281
  end
283
-
282
+
284
283
  service_with_ticket = service + query_separator + "ticket=" + st.ticket
285
284
  service_with_ticket
286
285
  end
287
-
286
+
288
287
  # Strips CAS-related parameters from a service URL and normalizes it,
289
288
  # removing trailing / and ?. Also converts any spaces to +.
290
289
  #
@@ -299,17 +298,18 @@ module CASServer::CAS
299
298
  return dirty_service if dirty_service.blank?
300
299
  clean_service = dirty_service.dup
301
300
  ['service', 'ticket', 'gateway', 'renew'].each do |p|
302
- clean_service.sub!(Regexp.new("#{p}=[^&]*"), '')
301
+ clean_service.sub!(Regexp.new("&?#{p}=[^&]*"), '')
303
302
  end
304
-
305
- clean_service.gsub!(/[\/\?]$/, '')
303
+
304
+ clean_service.gsub!(/[\/\?&]$/, '') # remove trailing ?, /, or &
305
+ clean_service.gsub!('?&', '?')
306
306
  clean_service.gsub!(' ', '+')
307
-
307
+
308
308
  $LOG.debug("Cleaned dirty service URL #{dirty_service.inspect} to #{clean_service.inspect}") if
309
309
  dirty_service != clean_service
310
-
310
+
311
311
  return clean_service
312
312
  end
313
313
  module_function :clean_service_url
314
-
314
+
315
315
  end