gunark-rubycas-server 0.6.99.336

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 (72) hide show
  1. data/CHANGELOG.txt +1 -0
  2. data/History.txt +245 -0
  3. data/LICENSE.txt +504 -0
  4. data/Manifest.txt +74 -0
  5. data/PostInstall.txt +3 -0
  6. data/README.txt +25 -0
  7. data/Rakefile +4 -0
  8. data/bin/rubycas-server +26 -0
  9. data/bin/rubycas-server-ctl +22 -0
  10. data/config.example.yml +442 -0
  11. data/config/hoe.rb +76 -0
  12. data/config/requirements.rb +15 -0
  13. data/custom_views.example.rb +11 -0
  14. data/lib/casserver.rb +111 -0
  15. data/lib/casserver/authenticators/active_directory_ldap.rb +11 -0
  16. data/lib/casserver/authenticators/base.rb +48 -0
  17. data/lib/casserver/authenticators/client_certificate.rb +46 -0
  18. data/lib/casserver/authenticators/ldap.rb +138 -0
  19. data/lib/casserver/authenticators/ntlm.rb +88 -0
  20. data/lib/casserver/authenticators/open_id.rb +22 -0
  21. data/lib/casserver/authenticators/sql.rb +102 -0
  22. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  23. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  24. data/lib/casserver/authenticators/test.rb +19 -0
  25. data/lib/casserver/cas.rb +308 -0
  26. data/lib/casserver/conf.rb +112 -0
  27. data/lib/casserver/controllers.rb +452 -0
  28. data/lib/casserver/environment.rb +26 -0
  29. data/lib/casserver/models.rb +218 -0
  30. data/lib/casserver/postambles.rb +174 -0
  31. data/lib/casserver/utils.rb +30 -0
  32. data/lib/casserver/version.rb +9 -0
  33. data/lib/casserver/views.rb +243 -0
  34. data/lib/rubycas-server.rb +1 -0
  35. data/lib/rubycas-server/version.rb +1 -0
  36. data/lib/themes/cas.css +121 -0
  37. data/lib/themes/notice.png +0 -0
  38. data/lib/themes/ok.png +0 -0
  39. data/lib/themes/simple/bg.png +0 -0
  40. data/lib/themes/simple/login_box_bg.png +0 -0
  41. data/lib/themes/simple/logo.png +0 -0
  42. data/lib/themes/simple/theme.css +28 -0
  43. data/lib/themes/urbacon/bg.png +0 -0
  44. data/lib/themes/urbacon/login_box_bg.png +0 -0
  45. data/lib/themes/urbacon/logo.png +0 -0
  46. data/lib/themes/urbacon/theme.css +33 -0
  47. data/lib/themes/warning.png +0 -0
  48. data/misc/basic_cas_single_signon_mechanism_diagram.png +0 -0
  49. data/misc/basic_cas_single_signon_mechanism_diagram.svg +652 -0
  50. data/resources/init.d.sh +58 -0
  51. data/script/console +10 -0
  52. data/script/destroy +14 -0
  53. data/script/generate +14 -0
  54. data/script/txt2html +82 -0
  55. data/setup.rb +1585 -0
  56. data/tasks/deployment.rake +34 -0
  57. data/tasks/environment.rake +7 -0
  58. data/tasks/website.rake +17 -0
  59. data/vendor/isaac_0.9.1/LICENSE +26 -0
  60. data/vendor/isaac_0.9.1/README +78 -0
  61. data/vendor/isaac_0.9.1/TODO +3 -0
  62. data/vendor/isaac_0.9.1/VERSIONS +3 -0
  63. data/vendor/isaac_0.9.1/crypt/ISAAC.rb +171 -0
  64. data/vendor/isaac_0.9.1/isaac.gemspec +39 -0
  65. data/vendor/isaac_0.9.1/setup.rb +596 -0
  66. data/vendor/isaac_0.9.1/test/TC_ISAAC.rb +76 -0
  67. data/website/index.html +40 -0
  68. data/website/index.txt +3 -0
  69. data/website/javascripts/rounded_corners_lite.inc.js +285 -0
  70. data/website/stylesheets/screen.css +138 -0
  71. data/website/template.html.erb +40 -0
  72. metadata +146 -0
@@ -0,0 +1,88 @@
1
+ # THIS AUTHENTICATOR DOES NOT WORK (not even close!)
2
+ #
3
+ # I started working on this but run into a wall, so I am commiting what I've got
4
+ # done and leaving it here with hopes of one day finishing it.
5
+ #
6
+ # The main problem is that although I've got the Lan Manager/NTLM password hash,
7
+ # I'm not sure what to do with it. i.e. I need to check it against the AD or SMB
8
+ # server or something... maybe faking an SMB share connection and using the LM
9
+ # response for authentication might do the trick?
10
+
11
+ require 'casserver/authenticators/base'
12
+
13
+ # Ruby/NTLM package from RubyForge
14
+ require 'net/ntlm'
15
+
16
+ module CASServer
17
+ module Authenticators
18
+ class NTLM
19
+ # This will have to be somehow called by the top of the 'get' method
20
+ # in the Login controller (maybe via a hook?)... if this code fails
21
+ # then the controller should fall back to some other method of authentication
22
+ # (probably AD/LDAP or something).
23
+ def filter_for_top_of_login_get_controller_method
24
+ $LOG.debug @env.inspect
25
+ if @env['HTTP_AUTHORIZATION'] =~ /NTLM ([^\s]+)/
26
+ # if we're here, then the client has sent back a Type1 or Type3 message
27
+ # in reply to our NTLM challenge or our Type2 message
28
+ data_raw = Base64.decode64($~[1])
29
+ $LOG.debug "T1 RAW: #{t1_raw}"
30
+ t = Net::NTLM::Message::Message.parse(t1_raw)
31
+ if t.kind_of? Net::NTLM::Type1
32
+ t1 = t
33
+ elsif t.kind_of? Net::NTLM::Type3
34
+ t3 = t
35
+ else
36
+ raise "Invalid NTLM reply from client."
37
+ end
38
+
39
+ if t1
40
+ $LOG.debug "T1: #{t1.inspect}"
41
+
42
+ # now put together a Type2 message asking for the client to send
43
+ # back NTLM credentials (LM hash and such)
44
+ t2 = Net::NTLM::Message::Type2.new
45
+ t2.set_flag :UNICODE
46
+ t2.set_flag :NTLM
47
+ t2.context = 0x0000000000000000 # this can probably just be left unassigned
48
+ t2.challenge = 0x0123456789abcdef # this should be a random 8-byte integer
49
+
50
+ $LOG.debug "T2: #{t2.inspect}"
51
+ $LOG.debug "T2: #{t2.serialize}"
52
+ headers["WWW-Authenticate"] = "NTLM #{t2.encode64}"
53
+
54
+ # the client should respond to this with a Type3 message...
55
+ r('401', '', headers)
56
+ return
57
+ else
58
+ # NOTE: for some reason the server never receives the T3 response, even though monitoring
59
+ # the HTTP traffic I can see that the client does send it back... there's probably
60
+ # another bug hiding somewhere here
61
+
62
+ lm_response = t3.lm_response
63
+ ntlm_response = t3.ntlm_response
64
+ username = t3.user
65
+ # this is where we run up against a wall... we need some way to check the lm and/or ntlm
66
+ # reponse against the authentication server (probably Active Directory)... maybe a samba
67
+ # call would do it?
68
+ $LOG.debug "T3 LM: #{lm_response.inspect}"
69
+ $LOG.debug "T3 NTLM: #{ntlm_response.inspect}"
70
+
71
+ # assuming the authentication was successful, we'll now need to do something in the
72
+ # controller acting as if we'd received correct login credentials (i.e. proceed as if
73
+ # CAS authentication was successful).... if authentication failed, then we should
74
+ # just fall back to old-school web-based authentication, asking the user to enter
75
+ # their username and password the normal CAS way
76
+ end
77
+ else
78
+ # this sends the initial NTLM challenge, asking the browser
79
+ # to send back a Type1 message
80
+ headers['WWW-Authenticate'] = "NTLM"
81
+ headers['Connection'] = "Close"
82
+ r('401', '', headers)
83
+ return
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,22 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ require 'openid'
4
+ require 'openid/extensions/sreg'
5
+ require 'openid/extensions/pape'
6
+ require 'openid/store/memory'
7
+
8
+
9
+ # CURRENTLY UNIMPLEMENTED
10
+ # This is just starter code.
11
+ class CASServer::Authenticators::OpenID < CASServer::Authenticators::Base
12
+
13
+ def validate(credentials)
14
+ raise NotImplementedError, "The OpenID authenticator is not yet implemented. "+
15
+ "See http://code.google.com/p/rubycas-server/issues/detail?id=36 if you are interested in helping this along."
16
+
17
+ read_standard_credentials(credentials)
18
+
19
+ store = OpenID::Store::Memory.new
20
+ consumer = OpenID::Consumer.new({}, store)
21
+ end
22
+ end
@@ -0,0 +1,102 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ begin
4
+ require 'active_record'
5
+ rescue LoadError
6
+ require 'rubygems'
7
+ require 'active_record'
8
+ end
9
+
10
+ # Authenticates against a plain SQL table.
11
+ #
12
+ # This assumes that all of your users are stored in a table that has a 'username'
13
+ # column and a 'password' column. When the user logs in, CAS conects to the
14
+ # database and looks for a matching username/password in the users table. If a
15
+ # matching username and password is found, authentication is successful.
16
+ #
17
+ # Any database backend supported by ActiveRecord can be used.
18
+ #
19
+ # Config example:
20
+ #
21
+ # authenticator:
22
+ # class: CASServer::Authenticators::SQL
23
+ # database:
24
+ # adapter: mysql
25
+ # database: some_database_with_users_table
26
+ # username: root
27
+ # password:
28
+ # server: localhost
29
+ # user_table: users
30
+ # username_column: username
31
+ # password_column: password
32
+ #
33
+ # When replying to a CAS client's validation request, the server will normally
34
+ # provide the client with the authenticated user's username. However it is now
35
+ # possible for the server to provide the client with additional attributes.
36
+ # You can configure the SQL authenticator to provide data from additional
37
+ # columns in the users table by listing the names of the columns under the
38
+ # 'extra_attributes' option. Note though that this functionality is experimental.
39
+ # It should work with RubyCAS-Client, but may or may not work with other CAS
40
+ # clients.
41
+ #
42
+ # For example, with this configuration, the 'full_name' and 'access_level'
43
+ # columns will be provided to your CAS clients along with the username:
44
+ #
45
+ # authenticator:
46
+ # class: CASServer::Authenticators::SQL
47
+ # database:
48
+ # adapter: mysql
49
+ # database: some_database_with_users_table
50
+ # user_table: users
51
+ # username_column: username
52
+ # password_column: password
53
+ # extra_attributes: full_name, access_level
54
+ #
55
+ class CASServer::Authenticators::SQL < CASServer::Authenticators::Base
56
+
57
+ def validate(credentials)
58
+ read_standard_credentials(credentials)
59
+
60
+ raise CASServer::AuthenticatorError, "Cannot validate credentials because the authenticator hasn't yet been configured" unless @options
61
+ raise CASServer::AuthenticatorError, "Invalid authenticator configuration!" unless @options[:database]
62
+
63
+ CASUser.establish_connection @options[:database]
64
+ CASUser.set_table_name @options[:user_table] || "users"
65
+
66
+ username_column = @options[:username_column] || 'username'
67
+ password_column = @options[:password_column] || 'password'
68
+
69
+ results = CASUser.find(:all, :conditions => ["#{username_column} = ? AND #{password_column} = ?", @username, @password])
70
+
71
+ if results.size > 0
72
+ $LOG.warn("#{self.class}: Multiple matches found for user #{@username.inspect}") if results.size > 1
73
+
74
+ unless @options[:extra_attributes].blank?
75
+ if results.size > 1
76
+ $LOG.warn("#{self.class}: Unable to extract extra_attributes because multiple matches were found for #{@username.inspect}")
77
+ else
78
+ user = results.first
79
+
80
+ @extra_attributes = {}
81
+ extra_attributes_to_extract.each do |col|
82
+ @extra_attributes[col] = user.send(col)
83
+ end
84
+
85
+ if @extra_attributes.empty?
86
+ $LOG.warn("#{self.class}: Did not read any extra_attributes for user #{@username.inspect} even though an :extra_attributes option was provided.")
87
+ else
88
+ $LOG.debug("#{self.class}: Read the following extra_attributes for user #{@username.inspect}: #{@extra_attributes.inspect}")
89
+ end
90
+ end
91
+ end
92
+
93
+ return true
94
+ else
95
+ return false
96
+ end
97
+ end
98
+
99
+ class CASUser < ActiveRecord::Base
100
+ end
101
+
102
+ end
@@ -0,0 +1,75 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ require 'digest/sha1'
4
+ require 'digest/sha2'
5
+
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
17
+ # rather than being stored in plain text.
18
+ #
19
+ # Based on code contributed by Ben Mabey.
20
+ #
21
+ # Using this authenticator requires some configuration on the client side. Please see
22
+ # 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.
48
+ #
49
+ # Your model must have an 'encrypted_password' column where the password will be stored,
50
+ # and an 'encryption_salt' column that will be populated with a random string before
51
+ # the user record is first created.
52
+ module EncryptedPassword
53
+ def self.included(mod)
54
+ raise "#{self} should be inclued in an ActiveRecord class!" unless mod.respond_to?(:before_save)
55
+ mod.before_save :generate_encryption_salt
56
+ end
57
+
58
+ def encrypt(str)
59
+ Digest::SHA256.hexdigest("#{encryption_salt}::#{str}")
60
+ end
61
+
62
+ def password=(password)
63
+ self[:encrypted_password] = encrypt(password)
64
+ end
65
+
66
+ def generate_encryption_salt
67
+ self.encryption_salt = Digest::SHA1.hexdigest(Crypt::ISAAC.new.rand(2**31).to_s) unless
68
+ encryption_salt
69
+ end
70
+ end
71
+
72
+ class CASUser < ActiveRecord::Base
73
+ include EncryptedPassword
74
+ end
75
+ end
@@ -0,0 +1,19 @@
1
+ require 'casserver/authenticators/sql'
2
+
3
+ require 'digest/md5'
4
+
5
+ # Essentially the same as the standard SQL authenticator, but this version
6
+ # assumes that your password is stored as an MD5 hash.
7
+ #
8
+ # This was contributed by malcomm for Drupal authentication. To work with
9
+ # Drupal, you should use 'name' for the :username_column config option, and
10
+ # 'pass' for the :password_column.
11
+ class CASServer::Authenticators::SQLMd5 < CASServer::Authenticators::SQL
12
+
13
+ protected
14
+ def read_standard_credentials(credentials)
15
+ super
16
+ @password = Digest::MD5.hexdigest(@password)
17
+ end
18
+
19
+ end
@@ -0,0 +1,19 @@
1
+ require 'casserver/authenticators/base'
2
+
3
+ # Dummy authenticator used for testing.
4
+ # Accepts "testuser" for username and "testpassword" for password; otherwise authentication fails.
5
+ # Raises an AuthenticationError when username is "do_error" (this is useful to test the Exception
6
+ # handling functionality).
7
+ class CASServer::Authenticators::Test < CASServer::Authenticators::Base
8
+ def validate(credentials)
9
+ read_standard_credentials(credentials)
10
+
11
+ raise CASServer::AuthenticatorError, "Username is 'do_error'!" if @username == 'do_error'
12
+
13
+ @extra_attributes[:test_string] = "testing!"
14
+ @extra_attributes[:test_numeric] = 123.45
15
+ @extra_attributes[:test_serialized] = {:foo => 'bar', :alpha => [1,2,3]}
16
+
17
+ return @password == "testpassword"
18
+ end
19
+ end
@@ -0,0 +1,308 @@
1
+ require 'uri'
2
+ require 'net/https'
3
+
4
+ # Encapsulates CAS functionality. This module is meant to be included in
5
+ # the CASServer::Controllers module.
6
+ module CASServer::CAS
7
+
8
+ include CASServer::Models
9
+
10
+ def generate_login_ticket
11
+ # 3.5 (login ticket)
12
+ lt = LoginTicket.new
13
+ lt.ticket = "LT-" + CASServer::Utils.random_string
14
+ lt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
15
+ lt.save!
16
+ $LOG.debug("Generated login ticket '#{lt.ticket}' for client" +
17
+ " at '#{lt.client_hostname}'")
18
+ lt
19
+ end
20
+
21
+ # Creates a TicketGrantingTicket for the given username. This is done when the user logs in
22
+ # for the first time to establish their SSO session (after their credentials have been validated).
23
+ #
24
+ # 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
26
+ # validation requests from clients.
27
+ def generate_ticket_granting_ticket(username, extra_attributes = {})
28
+ # 3.6 (ticket granting cookie/ticket)
29
+ tgt = TicketGrantingTicket.new
30
+ tgt.ticket = "TGC-" + CASServer::Utils.random_string
31
+ tgt.username = username
32
+ tgt.extra_attributes = extra_attributes
33
+ tgt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
34
+ tgt.save!
35
+ $LOG.debug("Generated ticket granting ticket '#{tgt.ticket}' for user" +
36
+ " '#{tgt.username}' at '#{tgt.client_hostname}'" +
37
+ (extra_attributes.blank? ? "" : " with extra attributes #{extra_attributes.inspect}"))
38
+ tgt
39
+ end
40
+
41
+ def generate_service_ticket(service, username, tgt)
42
+ # 3.1 (service ticket)
43
+ st = ServiceTicket.new
44
+ st.ticket = "ST-" + CASServer::Utils.random_string
45
+ st.service = service
46
+ st.username = username
47
+ st.ticket_granting_ticket = tgt
48
+ st.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
49
+ st.save!
50
+ $LOG.debug("Generated service ticket '#{st.ticket}' for service '#{st.service}'" +
51
+ " for user '#{st.username}' at '#{st.client_hostname}'")
52
+ st
53
+ end
54
+
55
+ def generate_proxy_ticket(target_service, pgt)
56
+ # 3.2 (proxy ticket)
57
+ pt = ProxyTicket.new
58
+ pt.ticket = "PT-" + CASServer::Utils.random_string
59
+ pt.service = target_service
60
+ 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.save!
65
+ $LOG.debug("Generated proxy ticket '#{pt.ticket}' for target service '#{pt.service}'" +
66
+ " for user '#{pt.username}' at '#{pt.client_hostname}' using proxy-granting" +
67
+ " ticket '#{pgt.ticket}'")
68
+ pt
69
+ end
70
+
71
+ def generate_proxy_granting_ticket(pgt_url, st)
72
+ uri = URI.parse(pgt_url)
73
+ https = Net::HTTP.new(uri.host,uri.port)
74
+ https.use_ssl = true
75
+
76
+ # Here's what's going on here:
77
+ #
78
+ # 1. We generate a ProxyGrantingTicket (but don't store it in the database just yet)
79
+ # 2. Deposit the PGT and it's associated IOU at the proxy callback URL.
80
+ # 3. If the proxy callback URL responds with HTTP code 200, store the PGT and return it;
81
+ # otherwise don't save it and return nothing.
82
+ #
83
+ https.start do |conn|
84
+ path = uri.path.empty? ? '/' : uri.path
85
+
86
+ pgt = ProxyGrantingTicket.new
87
+ pgt.ticket = "PGT-" + CASServer::Utils.random_string(60)
88
+ pgt.iou = "PGTIOU-" + CASServer::Utils.random_string(57)
89
+ pgt.service_ticket_id = st.id
90
+ pgt.client_hostname = env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']
91
+
92
+ # FIXME: The CAS protocol spec says to use 'pgt' as the parameter, but in practice
93
+ # the JA-SIG and Yale server implementations use pgtId. We'll go with the
94
+ # in-practice standard.
95
+ path += (uri.query.nil? || uri.query.empty? ? '?' : '&') + "pgtId=#{pgt.ticket}&pgtIou=#{pgt.iou}"
96
+
97
+ response = conn.request_get(path)
98
+ # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
99
+
100
+ if response.code.to_i == 200
101
+ # 3.4 (proxy-granting ticket IOU)
102
+ pgt.save!
103
+ $LOG.debug "PGT generated for pgt_url '#{pgt_url}': #{pgt.inspect}"
104
+ pgt
105
+ else
106
+ $LOG.warn "PGT callback server responded with a bad result code '#{response.code}'. PGT will not be stored."
107
+ end
108
+ end
109
+ end
110
+
111
+ def validate_login_ticket(ticket)
112
+ $LOG.debug("Validating login ticket '#{ticket}'")
113
+
114
+ success = false
115
+ 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.")
118
+ elsif lt = LoginTicket.find_by_ticket(ticket)
119
+ 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")
124
+ else
125
+ error = "Your login ticket has expired. Please try logging in again."
126
+ $LOG.warn("Expired login ticket '#{ticket}'")
127
+ end
128
+ else
129
+ error = "The login ticket you provided is invalid. Please try logging in again."
130
+ $LOG.warn("Invalid login ticket '#{ticket}'")
131
+ end
132
+
133
+ lt.consume! if lt
134
+
135
+ error
136
+ end
137
+
138
+ def validate_ticket_granting_ticket(ticket)
139
+ $LOG.debug("Validating ticket granting ticket '#{ticket}'")
140
+
141
+ if ticket.nil?
142
+ error = "No ticket granting ticket given."
143
+ $LOG.debug(error)
144
+ elsif tgt = TicketGrantingTicket.find_by_ticket(ticket)
145
+ if CASServer::Conf.expire_sessions && Time.now - tgt.created_on > CASServer::Conf.ticket_granting_ticket_expiry
146
+ error = "Your session has expired. Please log in again."
147
+ $LOG.info("Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired.")
148
+ else
149
+ $LOG.info("Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated.")
150
+ end
151
+ else
152
+ error = "Invalid ticket granting ticket '#{ticket}' (no matching ticket found in the database)."
153
+ $LOG.warn(error)
154
+ end
155
+
156
+ [tgt, error]
157
+ end
158
+
159
+ def validate_service_ticket(service, ticket, allow_proxy_tickets = false)
160
+ $LOG.debug("Validating service/proxy ticket '#{ticket}' for service '#{service}'")
161
+
162
+ if service.nil? or ticket.nil?
163
+ error = Error.new(:INVALID_REQUEST, "Ticket or service parameter was missing in the request.")
164
+ $LOG.warn("#{error.code} - #{error.message}")
165
+ elsif st = ServiceTicket.find_by_ticket(ticket)
166
+ if st.consumed?
167
+ 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
170
+ 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
173
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has expired.")
174
+ $LOG.warn("Ticket '#{ticket}' has expired.")
175
+ elsif !st.matches_service? service
176
+ error = Error.new(:INVALID_SERVICE, "The ticket '#{ticket}' belonging to user '#{st.username}' is valid,"+
177
+ " but the requested service '#{service}' does not match the service '#{st.service}' associated with this ticket.")
178
+ $LOG.warn("#{error.code} - #{error.message}")
179
+ else
180
+ $LOG.info("Ticket '#{ticket}' for service '#{service}' for user '#{st.username}' successfully validated.")
181
+ end
182
+ else
183
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' not recognized.")
184
+ $LOG.warn("#{error.code} - #{error.message}")
185
+ end
186
+
187
+ if st
188
+ st.consume!
189
+ end
190
+
191
+
192
+ [st, error]
193
+ end
194
+
195
+ def validate_proxy_ticket(service, ticket)
196
+ 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
200
+ 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}'"+
203
+ " (associated with proxy ticket '#{pt}' and belonging to user '#{pt.username}' is not associated with a service ticket.")
204
+ end
205
+ end
206
+
207
+ [pt, error]
208
+ end
209
+
210
+ def validate_proxy_granting_ticket(ticket)
211
+ if ticket.nil?
212
+ error = Error.new(:INVALID_REQUEST, "pgt parameter was missing in the request.")
213
+ $LOG.warn("#{error.code} - #{error.message}")
214
+ elsif pgt = ProxyGrantingTicket.find_by_ticket(ticket)
215
+ if pgt.service_ticket
216
+ $LOG.info("Proxy granting ticket '#{ticket}' belonging to user '#{pgt.service_ticket.username}' successfully validated.")
217
+ else
218
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{ticket}' is not associated with a service ticket.")
219
+ $LOG.error("#{error.code} - #{error.message}")
220
+ end
221
+ else
222
+ error = Error.new(:BAD_PGT, "Invalid proxy granting ticket '#{ticket}' (no matching ticket found in the database).")
223
+ $LOG.warn("#{error.code} - #{error.message}")
224
+ end
225
+
226
+ [pgt, error]
227
+ end
228
+
229
+ # Takes an existing ServiceTicket object (presumably pulled from the database)
230
+ # and sends a POST with logout information to the service that the ticket
231
+ # was generated for.
232
+ #
233
+ # This makes possible the "single sign-out" functionality added in CAS 3.1.
234
+ # See http://www.ja-sig.org/wiki/display/CASUM/Single+Sign+Out
235
+ def send_logout_notification_for_service_ticket(st)
236
+ uri = URI.parse(st.service)
237
+ http = Net::HTTP.new(uri.host,uri.port)
238
+ #http.use_ssl = true if uri.scheme = 'https'
239
+
240
+ http.start do |conn|
241
+ path = uri.path
242
+ path = '/' if path.empty?
243
+
244
+ time = Time.now
245
+ rand = CASServer::Utils.random_string
246
+
247
+ data = %{<samlp:LogoutRequest ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
248
+ <saml:NameID></saml:NameID>
249
+ <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
250
+ </samlp:LogoutRequest>}
251
+
252
+ response = conn.request_post(path, data)
253
+
254
+ if response.code.to_i == 200
255
+ $LOG.info "Logout notification successfully posted to #{st.service.inspect}."
256
+ return true
257
+ else
258
+ $LOG.error "Service #{st.service.inspect} responed to logout notification with code '#{response.code}'."
259
+ return false
260
+ end
261
+ end
262
+ end
263
+
264
+ def service_uri_with_ticket(service, st)
265
+ raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? CASServer::Models::ServiceTicket
266
+
267
+ # This will choke with a URI::InvalidURIError if service URI is not properly URI-escaped...
268
+ # This exception is handled further upstream (i.e. in the controller).
269
+ service_uri = URI.parse(service)
270
+
271
+ if service.include? "?"
272
+ if service_uri.query.empty?
273
+ query_separator = ""
274
+ else
275
+ query_separator = "&"
276
+ end
277
+ else
278
+ query_separator = "?"
279
+ end
280
+
281
+ service_with_ticket = service + query_separator + "ticket=" + st.ticket
282
+ service_with_ticket
283
+ end
284
+
285
+ # Strips CAS-related parameters from a service URL and normalizes it,
286
+ # removing trailing / and ?.
287
+ #
288
+ # For example, "http://google.com?ticket=12345" will be returned as
289
+ # "http://google.com". Also, "http://google.com/" would be returned as
290
+ # "http://google.com".
291
+ #
292
+ # Note that only the first occurance of each CAS-related parameter is
293
+ # removed, so that "http://google.com?ticket=12345&ticket=abcd" would be
294
+ # returned as "http://google.com?ticket=abcd".
295
+ def clean_service_url(dirty_service)
296
+ return dirty_service if dirty_service.blank?
297
+ clean_service = dirty_service.dup
298
+ ['service', 'ticket', 'gateway', 'renew'].each do |p|
299
+ clean_service.sub!(Regexp.new("#{p}=[^&]*"), '')
300
+ end
301
+
302
+ clean_service.gsub!(/[\/\?]$/, '')
303
+
304
+ return clean_service
305
+ end
306
+ module_function :clean_service_url
307
+
308
+ end