rubycas-server 0.7.1.1 → 1.0

Sign up to get free protection for your applications and to get access to all the features.
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