uchouhan-rubycas-server 1.0.a

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 (83) hide show
  1. data/CHANGELOG +289 -0
  2. data/LICENSE +26 -0
  3. data/README.md +19 -0
  4. data/Rakefile +1 -0
  5. data/bin/rubycas-server +16 -0
  6. data/bin/rubycas-server-ctl +9 -0
  7. data/lib/casserver.rb +13 -0
  8. data/lib/casserver/authenticators/active_directory_ldap.rb +19 -0
  9. data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
  10. data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
  11. data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
  12. data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
  13. data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
  14. data/lib/casserver/authenticators/base.rb +67 -0
  15. data/lib/casserver/authenticators/client_certificate.rb +47 -0
  16. data/lib/casserver/authenticators/google.rb +58 -0
  17. data/lib/casserver/authenticators/ldap.rb +147 -0
  18. data/lib/casserver/authenticators/ntlm.rb +88 -0
  19. data/lib/casserver/authenticators/open_id.rb +22 -0
  20. data/lib/casserver/authenticators/sql.rb +133 -0
  21. data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
  22. data/lib/casserver/authenticators/sql_encrypted.rb +77 -0
  23. data/lib/casserver/authenticators/sql_md5.rb +19 -0
  24. data/lib/casserver/authenticators/sql_rest_auth.rb +85 -0
  25. data/lib/casserver/authenticators/tacc.rb +67 -0
  26. data/lib/casserver/authenticators/test.rb +21 -0
  27. data/lib/casserver/cas.rb +327 -0
  28. data/lib/casserver/localization.rb +91 -0
  29. data/lib/casserver/model.rb +269 -0
  30. data/lib/casserver/server.rb +623 -0
  31. data/lib/casserver/utils.rb +32 -0
  32. data/lib/casserver/views/_login_form.erb +41 -0
  33. data/lib/casserver/views/layout.erb +17 -0
  34. data/lib/casserver/views/login.erb +29 -0
  35. data/lib/casserver/views/proxy.builder +11 -0
  36. data/lib/casserver/views/proxy_validate.builder +26 -0
  37. data/lib/casserver/views/service_validate.builder +19 -0
  38. data/lib/casserver/views/validate.erb +1 -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/public/themes/notice.png +0 -0
  51. data/public/themes/ok.png +0 -0
  52. data/public/themes/simple/bg.png +0 -0
  53. data/public/themes/simple/favicon.png +0 -0
  54. data/public/themes/simple/login_box_bg.png +0 -0
  55. data/public/themes/simple/logo.png +0 -0
  56. data/public/themes/simple/theme.css +28 -0
  57. data/public/themes/tadnet/bg.png +0 -0
  58. data/public/themes/tadnet/button.png +0 -0
  59. data/public/themes/tadnet/favicon.png +0 -0
  60. data/public/themes/tadnet/login_box_bg.png +0 -0
  61. data/public/themes/tadnet/logo.png +0 -0
  62. data/public/themes/tadnet/theme.css +55 -0
  63. data/public/themes/urbacon/bg.png +0 -0
  64. data/public/themes/urbacon/login_box_bg.png +0 -0
  65. data/public/themes/urbacon/logo.png +0 -0
  66. data/public/themes/urbacon/theme.css +33 -0
  67. data/public/themes/warning.png +0 -0
  68. data/resources/config.example.yml +574 -0
  69. data/resources/config.ru +42 -0
  70. data/resources/custom_views.example.rb +11 -0
  71. data/resources/init.d.sh +58 -0
  72. data/rubycas-server.gemspec +40 -0
  73. data/setup.rb +1585 -0
  74. data/spec/alt_config.yml +46 -0
  75. data/spec/casserver_spec.rb +114 -0
  76. data/spec/default_config.yml +46 -0
  77. data/spec/spec.opts +4 -0
  78. data/spec/spec_helper.rb +89 -0
  79. data/tasks/bundler.rake +4 -0
  80. data/tasks/db/migrate.rake +12 -0
  81. data/tasks/localization.rake +13 -0
  82. data/tasks/spec.rake +10 -0
  83. metadata +172 -0
@@ -0,0 +1,269 @@
1
+ require 'active_record'
2
+ require 'active_record/base'
3
+
4
+ module CASServer::Model
5
+
6
+ module Consumable
7
+ def consume!
8
+ self.consumed = Time.now
9
+ self.save!
10
+ end
11
+
12
+ def self.included(mod)
13
+ mod.extend(ClassMethods)
14
+ end
15
+
16
+ module ClassMethods
17
+ def cleanup(max_lifetime, max_unconsumed_lifetime)
18
+ transaction do
19
+ conditions = ["created_on < ? OR (consumed IS NULL AND created_on < ?)",
20
+ Time.now - max_lifetime,
21
+ Time.now - max_unconsumed_lifetime]
22
+ expired_tickets_count = count(:conditions => conditions)
23
+
24
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
25
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
26
+
27
+ destroy_all(conditions)
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ class Base < ActiveRecord::Base
34
+ end
35
+
36
+ class Ticket < Base
37
+ def to_s
38
+ ticket
39
+ end
40
+
41
+ def self.cleanup(max_lifetime)
42
+ transaction do
43
+ conditions = ["created_on < ?", Time.now - max_lifetime]
44
+ expired_tickets_count = count(:conditions => conditions)
45
+
46
+ $LOG.debug("Destroying #{expired_tickets_count} expired #{self.name.demodulize}"+
47
+ "#{'s' if expired_tickets_count > 1}.") if expired_tickets_count > 0
48
+
49
+ destroy_all(conditions)
50
+ end
51
+ end
52
+ end
53
+
54
+ class LoginTicket < Ticket
55
+ set_table_name 'casserver_lt'
56
+ include Consumable
57
+ end
58
+
59
+ class ServiceTicket < Ticket
60
+ set_table_name 'casserver_st'
61
+ include Consumable
62
+
63
+ belongs_to :granted_by_tgt,
64
+ :class_name => 'CASServer::Model::TicketGrantingTicket',
65
+ :foreign_key => :granted_by_tgt_id
66
+ has_one :proxy_granting_ticket,
67
+ :foreign_key => :created_by_st_id
68
+
69
+ def matches_service?(service)
70
+ CASServer::CAS.clean_service_url(self.service) ==
71
+ CASServer::CAS.clean_service_url(service)
72
+ end
73
+ end
74
+
75
+ class ProxyTicket < ServiceTicket
76
+ belongs_to :granted_by_pgt,
77
+ :class_name => 'CASServer::Model::ProxyGrantingTicket',
78
+ :foreign_key => :granted_by_pgt_id
79
+ end
80
+
81
+ class TicketGrantingTicket < Ticket
82
+ set_table_name 'casserver_tgt'
83
+
84
+ serialize :extra_attributes
85
+
86
+ has_many :granted_service_tickets,
87
+ :class_name => 'CASServer::Model::ServiceTicket',
88
+ :foreign_key => :granted_by_tgt_id
89
+ end
90
+
91
+ class ProxyGrantingTicket < Ticket
92
+ set_table_name 'casserver_pgt'
93
+ belongs_to :service_ticket
94
+ has_many :granted_proxy_tickets,
95
+ :class_name => 'CASServer::Model::ProxyTicket',
96
+ :foreign_key => :granted_by_pgt_id
97
+ end
98
+
99
+ class Error
100
+ attr_reader :code, :message
101
+
102
+ def initialize(code, message)
103
+ @code = code
104
+ @message = message
105
+ end
106
+
107
+ def to_s
108
+ message
109
+ end
110
+ end
111
+
112
+ # class CreateCASServer < V 0.1
113
+ # def self.up
114
+ # if ActiveRecord::Base.connection.table_alias_length > 30
115
+ # $LOG.info("Creating database with long table names...")
116
+ #
117
+ # create_table :casserver_login_tickets, :force => true do |t|
118
+ # t.column :ticket, :string, :null => false
119
+ # t.column :created_on, :timestamp, :null => false
120
+ # t.column :consumed, :datetime, :null => true
121
+ # t.column :client_hostname, :string, :null => false
122
+ # end
123
+ #
124
+ # create_table :casserver_service_tickets, :force => true do |t|
125
+ # t.column :ticket, :string, :null => false
126
+ # t.column :service, :string, :null => false
127
+ # t.column :created_on, :timestamp, :null => false
128
+ # t.column :consumed, :datetime, :null => true
129
+ # t.column :client_hostname, :string, :null => false
130
+ # t.column :username, :string, :null => false
131
+ # t.column :type, :string, :null => false
132
+ # t.column :proxy_granting_ticket_id, :integer, :null => true
133
+ # end
134
+ #
135
+ # create_table :casserver_ticket_granting_tickets, :force => true do |t|
136
+ # t.column :ticket, :string, :null => false
137
+ # t.column :created_on, :timestamp, :null => false
138
+ # t.column :client_hostname, :string, :null => false
139
+ # t.column :username, :string, :null => false
140
+ # end
141
+ #
142
+ # create_table :casserver_proxy_granting_tickets, :force => true do |t|
143
+ # t.column :ticket, :string, :null => false
144
+ # t.column :created_on, :timestamp, :null => false
145
+ # t.column :client_hostname, :string, :null => false
146
+ # t.column :iou, :string, :null => false
147
+ # t.column :service_ticket_id, :integer, :null => false
148
+ # end
149
+ # end
150
+ # end
151
+ #
152
+ # def self.down
153
+ # if ActiveRecord::Base.connection.table_alias_length > 30
154
+ # drop_table :casserver_proxy_granting_tickets
155
+ # drop_table :casserver_ticket_granting_tickets
156
+ # drop_table :casserver_service_tickets
157
+ # drop_table :casserver_login_tickets
158
+ # end
159
+ # end
160
+ # end
161
+ #
162
+ # # Oracle table names cannot exceed 30 chars...
163
+ # # See http://code.google.com/p/rubycas-server/issues/detail?id=15
164
+ # class ShortenTableNames < V 0.5
165
+ # def self.up
166
+ # if ActiveRecord::Base.connection.table_alias_length > 30
167
+ # $LOG.info("Shortening table names")
168
+ # rename_table :casserver_login_tickets, :casserver_lt
169
+ # rename_table :casserver_service_tickets, :casserver_st
170
+ # rename_table :casserver_ticket_granting_tickets, :casserver_tgt
171
+ # rename_table :casserver_proxy_granting_tickets, :casserver_pgt
172
+ # else
173
+ # create_table :casserver_lt, :force => true do |t|
174
+ # t.column :ticket, :string, :null => false
175
+ # t.column :created_on, :timestamp, :null => false
176
+ # t.column :consumed, :datetime, :null => true
177
+ # t.column :client_hostname, :string, :null => false
178
+ # end
179
+ #
180
+ # create_table :casserver_st, :force => true do |t|
181
+ # t.column :ticket, :string, :null => false
182
+ # t.column :service, :string, :null => false
183
+ # t.column :created_on, :timestamp, :null => false
184
+ # t.column :consumed, :datetime, :null => true
185
+ # t.column :client_hostname, :string, :null => false
186
+ # t.column :username, :string, :null => false
187
+ # t.column :type, :string, :null => false
188
+ # t.column :proxy_granting_ticket_id, :integer, :null => true
189
+ # end
190
+ #
191
+ # create_table :casserver_tgt, :force => true do |t|
192
+ # t.column :ticket, :string, :null => false
193
+ # t.column :created_on, :timestamp, :null => false
194
+ # t.column :client_hostname, :string, :null => false
195
+ # t.column :username, :string, :null => false
196
+ # end
197
+ #
198
+ # create_table :casserver_pgt, :force => true do |t|
199
+ # t.column :ticket, :string, :null => false
200
+ # t.column :created_on, :timestamp, :null => false
201
+ # t.column :client_hostname, :string, :null => false
202
+ # t.column :iou, :string, :null => false
203
+ # t.column :service_ticket_id, :integer, :null => false
204
+ # end
205
+ # end
206
+ # end
207
+ #
208
+ # def self.down
209
+ # if ActiveRecord::Base.connection.table_alias_length > 30
210
+ # rename_table :casserver_lt, :cassserver_login_tickets
211
+ # rename_table :casserver_st, :casserver_service_tickets
212
+ # rename_table :casserver_tgt, :casserver_ticket_granting_tickets
213
+ # rename_table :casserver_pgt, :casserver_proxy_granting_tickets
214
+ # else
215
+ # drop_table :casserver_pgt
216
+ # drop_table :casserver_tgt
217
+ # drop_table :casserver_st
218
+ # drop_table :casserver_lt
219
+ # end
220
+ # end
221
+ # end
222
+ #
223
+ # class AddTgtToSt < V 0.7
224
+ # def self.up
225
+ # add_column :casserver_st, :tgt_id, :integer, :null => true
226
+ # end
227
+ #
228
+ # def self.down
229
+ # remove_column :casserver_st, :tgt_id, :integer
230
+ # end
231
+ # end
232
+ #
233
+ # class ChangeServiceToText < V 0.71
234
+ # def self.up
235
+ # # using change_column to change the column type from :string to :text
236
+ # # doesn't seem to work, at least under MySQL, so we drop and re-create
237
+ # # the column instead
238
+ # remove_column :casserver_st, :service
239
+ # say "WARNING: All existing service tickets are being deleted."
240
+ # add_column :casserver_st, :service, :text
241
+ # end
242
+ #
243
+ # def self.down
244
+ # change_column :casserver_st, :service, :string
245
+ # end
246
+ # end
247
+ #
248
+ # class AddExtraAttributes < V 0.72
249
+ # def self.up
250
+ # add_column :casserver_tgt, :extra_attributes, :text
251
+ # end
252
+ #
253
+ # def self.down
254
+ # remove_column :casserver_tgt, :extra_attributes
255
+ # end
256
+ # end
257
+ #
258
+ # class RenamePgtForeignKeys < V 0.80
259
+ # def self.up
260
+ # rename_column :casserver_st, :proxy_granting_ticket_id, :granted_by_pgt_id
261
+ # rename_column :casserver_st, :tgt_id, :granted_by_tgt_id
262
+ # end
263
+ #
264
+ # def self.down
265
+ # rename_column :casserver_st, :granted_by_pgt_id, :proxy_granting_ticket_id
266
+ # rename_column :casserver_st, :granted_by_tgt_id, :tgt_id
267
+ # end
268
+ # end
269
+ end
@@ -0,0 +1,623 @@
1
+ require 'sinatra/base'
2
+
3
+ $: << File.expand_path(File.dirname(__FILE__)) + '/../../vendor/isaac_0.9.1'
4
+
5
+ require 'casserver/localization'
6
+ require 'casserver/utils'
7
+ require 'casserver/cas'
8
+
9
+ require 'logger'
10
+ $LOG ||= Logger.new(STDOUT)
11
+
12
+ module CASServer
13
+ class Server < Sinatra::Base
14
+ CONFIG_FILE = ENV['CONFIG_FILE'] || "/etc/rubycas-server/config.yml"
15
+
16
+ include CASServer::CAS # CAS protocol helpers
17
+ include Localization
18
+
19
+ set :app_file, __FILE__
20
+ set :public, File.expand_path(File.dirname(__FILE__)+"/../../public")
21
+
22
+ config = HashWithIndifferentAccess.new(
23
+ :maximum_unused_login_ticket_lifetime => 5.minutes,
24
+ :maximum_unused_service_ticket_lifetime => 5.minutes, # CAS Protocol Spec, sec. 3.2.1 (recommended expiry time)
25
+ :maximum_session_lifetime => 1.month, # all tickets are deleted after this period of time
26
+ :log => {:file => 'casserver.log', :level => 'DEBUG'},
27
+ :uri_path => ""
28
+ )
29
+ set :config, config
30
+
31
+ def self.uri_path
32
+ config[:uri_path]
33
+ end
34
+
35
+ def self.run!(options={})
36
+ set options
37
+
38
+ handler = detect_rack_handler
39
+ handler_name = handler.name.gsub(/.*::/, '')
40
+
41
+ puts "== RubyCAS-Server is starting up " +
42
+ "on port #{config[:port] || port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
43
+ handler.run self, handler_options do |server|
44
+ [:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
45
+ set :running, true
46
+ end
47
+ rescue Errno::EADDRINUSE => e
48
+ puts "== Something is already running on port #{port}!"
49
+ end
50
+
51
+ def self.quit!(server, handler_name)
52
+ ## Use thins' hard #stop! if available, otherwise just #stop
53
+ server.respond_to?(:stop!) ? server.stop! : server.stop
54
+ puts "\n== RubyCAS-Server is shutting down" unless handler_name =~/cgi/i
55
+ end
56
+
57
+ def self.load_config_file(config_file)
58
+ begin
59
+ config_file = File.open(config_file)
60
+ rescue Errno::ENOENT => e
61
+ $stderr.puts
62
+ $stderr.puts "!!! Config file #{config_file.inspect} does not exist!"
63
+ $stderr.puts
64
+ raise e
65
+ rescue Errno::EACCES => e
66
+ $stderr.puts
67
+ $stderr.puts "!!! Config file #{config_file.inspect} is not readable (permission denied)!"
68
+ $stderr.puts
69
+ raise e
70
+ rescue => e
71
+ $stderr.puts
72
+ $stderr.puts "!!! Config file #{config_file.inspect} could not be read!"
73
+ $stderr.puts
74
+ raise e
75
+ end
76
+
77
+ config.merge! HashWithIndifferentAccess.new(YAML.load(config_file))
78
+ set :server, config[:server] || 'webrick'
79
+ end
80
+
81
+ def self.reconfigure!(config)
82
+ config.each do |key, val|
83
+ self.config[key] = val
84
+ end
85
+ init_database!
86
+ init_logger!
87
+ init_authenticators!
88
+ end
89
+
90
+ def self.handler_options
91
+ handler_options = {
92
+ :Host => bind || config[:bind_address],
93
+ :Port => config[:port] || 443
94
+ }
95
+
96
+ handler_options.merge(handler_ssl_options).to_hash.symbolize_keys!
97
+ end
98
+
99
+ def self.handler_ssl_options
100
+ return {} unless config[:ssl_cert]
101
+
102
+ cert_path = config[:ssl_cert]
103
+ key_path = config[:ssl_key] || config[:ssl_cert]
104
+
105
+ unless cert_path.nil? && key_path.nil?
106
+ raise Error, "The ssl_cert and ssl_key options cannot be used with mongrel. You will have to run your " +
107
+ " server behind a reverse proxy if you want SSL under mongrel." if
108
+ config[:server] == 'mongrel'
109
+
110
+ raise Error, "The specified certificate file #{cert_path.inspect} does not exist or is not readable. " +
111
+ " Your 'ssl_cert' configuration setting must be a path to a valid " +
112
+ " ssl certificate." unless
113
+ File.exists? cert_path
114
+
115
+ raise Error, "The specified key file #{key_path.inspect} does not exist or is not readable. " +
116
+ " Your 'ssl_key' configuration setting must be a path to a valid " +
117
+ " ssl private key." unless
118
+ File.exists? key_path
119
+
120
+ require 'openssl'
121
+ require 'webrick/https'
122
+
123
+ cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
124
+ key = OpenSSL::PKey::RSA.new(File.read(key_path))
125
+
126
+ {
127
+ :SSLEnable => true,
128
+ :SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
129
+ :SSLCertificate => cert,
130
+ :SSLPrivateKey => key
131
+ }
132
+ end
133
+ end
134
+
135
+ def self.init_authenticators!
136
+ auth = []
137
+
138
+ begin
139
+ # attempt to instantiate the authenticator
140
+ config[:authenticator] = [config[:authenticator]] unless config[:authenticator].instance_of? Array
141
+ config[:authenticator].each { |authenticator| auth << authenticator[:class].constantize}
142
+ rescue NameError
143
+ if config[:authenticator].instance_of? Array
144
+ config[:authenticator].each do |authenticator|
145
+ if !authenticator[:source].nil?
146
+ # config.yml explicitly names source file
147
+ require authenticator[:source]
148
+ else
149
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
150
+ auth_rb = authenticator[:class].underscore.gsub('cas_server/', '')
151
+ require 'casserver/'+auth_rb
152
+ end
153
+ auth << authenticator[:class].constantize
154
+ end
155
+ else
156
+ if config[:authenticator][:source]
157
+ # config.yml explicitly names source file
158
+ require config[:authenticator][:source]
159
+ else
160
+ # the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
161
+ auth_rb = config[:authenticator][:class].underscore.gsub('cas_server/', '')
162
+ require 'casserver/'+auth_rb
163
+ end
164
+
165
+ auth << config[:authenticator][:class].constantize
166
+ config[:authenticator] = [config[:authenticator]]
167
+ end
168
+ end
169
+
170
+ auth.zip(config[:authenticator]).each_with_index{ |auth_conf, index|
171
+ authenticator, conf = auth_conf
172
+ $LOG.debug "About to setup #{authenticator} with #{conf.inspect}..."
173
+ authenticator.setup(conf.merge('auth_index' => index)) if authenticator.respond_to?(:setup)
174
+ $LOG.debug "Done setting up #{authenticator}."
175
+ }
176
+
177
+ set :auth, auth
178
+ end
179
+
180
+ def self.init_logger!
181
+ if config[:log]
182
+ if $LOG && config[:log][:file]
183
+ $LOG.debug "Redirecting log to #{config[:log][:file]}"
184
+ #$LOG.close
185
+ $LOG = Logger.new(config[:log][:file])
186
+ end
187
+ $LOG.debug "TEST"
188
+ $LOG.level = Logger.const_get(config[:log][:level]) if config[:log][:level]
189
+ end
190
+ end
191
+
192
+ def self.init_database!
193
+ #CASServer::Model::Base.establish_connection(config[:database])
194
+ ActiveRecord::Base.establish_connection(config[:database])
195
+ end
196
+
197
+ configure do
198
+ load_config_file(CONFIG_FILE)
199
+ init_logger!
200
+ init_database!
201
+ init_authenticators!
202
+ end
203
+
204
+ before do
205
+ GetText.locale = determine_locale(request)
206
+ content_type :html, 'charset' => 'utf-8'
207
+ @theme = settings.config[:theme]
208
+ @organization = settings.config[:organization]
209
+ end
210
+
211
+ # The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
212
+ # under http://www.ja-sig.org/products/cas/overview/protocol/index.html
213
+
214
+ # 2.1 :: Login
215
+
216
+ # 2.1.1
217
+ get "#{uri_path}/login" do
218
+ CASServer::Utils::log_controller_action(self.class, params)
219
+
220
+ # make sure there's no caching
221
+ headers['Pragma'] = 'no-cache'
222
+ headers['Cache-Control'] = 'no-store'
223
+ headers['Expires'] = (Time.now - 1.year).rfc2822
224
+
225
+ # optional params
226
+ @service = clean_service_url(params['service'])
227
+ @renew = params['renew']
228
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
229
+
230
+ if tgc = request.cookies['tgt']
231
+ tgt, tgt_error = validate_ticket_granting_ticket(tgc)
232
+ end
233
+
234
+ if tgt and !tgt_error
235
+ @message = {:type => 'notice',
236
+ :message => _("You are currently logged in as '%s'. If this is not you, please log in below.") % tgt.username }
237
+ end
238
+
239
+ if params['redirection_loop_intercepted']
240
+ @message = {:type => 'mistake',
241
+ :message => _("The client and server are unable to negotiate authentication. Please try logging in again later.")}
242
+ end
243
+
244
+ begin
245
+ if @service
246
+ if !@renew && tgt && !tgt_error
247
+ st = generate_service_ticket(@service, tgt.username, tgt)
248
+ service_with_ticket = service_uri_with_ticket(@service, st)
249
+ $LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
250
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
251
+ elsif @gateway
252
+ $LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
253
+ redirect @service, 303
254
+ end
255
+ elsif @gateway
256
+ $LOG.error("This is a gateway request but no service parameter was given!")
257
+ @message = {:type => 'mistake',
258
+ :message => _("The server cannot fulfill this gateway request because no service parameter was given.")}
259
+ end
260
+ rescue URI::InvalidURIError
261
+ $LOG.error("The service '#{@service}' is not a valid URI!")
262
+ @message = {:type => 'mistake',
263
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")}
264
+ end
265
+
266
+ lt = generate_login_ticket
267
+
268
+ $LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
269
+
270
+ @lt = lt.ticket
271
+
272
+ #$LOG.debug(env)
273
+
274
+ # If the 'onlyLoginForm' parameter is specified, we will only return the
275
+ # login form part of the page. This is useful for when you want to
276
+ # embed the login form in some external page (as an IFRAME, or otherwise).
277
+ # The optional 'submitToURI' parameter can be given to explicitly set the
278
+ # action for the form, otherwise the server will try to guess this for you.
279
+ if params.has_key? 'onlyLoginForm'
280
+ if @env['HTTP_HOST']
281
+ guessed_login_uri = "http#{@env['HTTPS'] && @env['HTTPS'] == 'on' ? 's' : ''}://#{@env['REQUEST_URI']}#{self / '/login'}"
282
+ else
283
+ guessed_login_uri = nil
284
+ end
285
+
286
+ @form_action = params['submitToURI'] || guessed_login_uri
287
+
288
+ if @form_action
289
+ render :login_form
290
+ else
291
+ status 500
292
+ render _("Could not guess the CAS login URI. Please supply a submitToURI parameter with your request.")
293
+ end
294
+ else
295
+ render(:erb, :login)
296
+ end
297
+ end
298
+
299
+
300
+ # 2.2
301
+ post "#{uri_path}/login" do
302
+ Utils::log_controller_action(self.class, params)
303
+
304
+ # 2.2.1 (optional)
305
+ @service = clean_service_url(params['service'])
306
+
307
+ # 2.2.2 (required)
308
+ @username = params['username']
309
+ @password = params['password']
310
+ @lt = params['lt']
311
+
312
+ # Remove leading and trailing widespace from username.
313
+ @username.strip! if @username
314
+
315
+ if @username && settings.config[:downcase_username]
316
+ $LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
317
+ @username.downcase!
318
+ end
319
+
320
+ if error = validate_login_ticket(@lt)
321
+ @message = {:type => 'mistake', :message => error}
322
+ # generate another login ticket to allow for re-submitting the form
323
+ @lt = generate_login_ticket.ticket
324
+ @status = 401
325
+ render :erb, :login
326
+ end
327
+
328
+ # generate another login ticket to allow for re-submitting the form after a post
329
+ @lt = generate_login_ticket.ticket
330
+
331
+ $LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{settings.auth.inspect}")
332
+
333
+ credentials_are_valid = false
334
+ extra_attributes = {}
335
+ successful_authenticator = nil
336
+ begin
337
+ auth_index = 0
338
+ settings.auth.each do |auth_class|
339
+ auth = auth_class.new
340
+
341
+ auth_config = settings.config[:authenticator][auth_index]
342
+ # pass the authenticator index to the configuration hash in case the authenticator needs to know
343
+ # it splace in the authenticator queue
344
+ auth.configure(auth_config.merge('auth_index' => auth_index))
345
+
346
+ credentials_are_valid = auth.validate(
347
+ :username => @username,
348
+ :password => @password,
349
+ :service => @service,
350
+ :request => @env
351
+ )
352
+ if credentials_are_valid
353
+ extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
354
+ successful_authenticator = auth
355
+ break
356
+ end
357
+
358
+ auth_index += 1
359
+ end
360
+ rescue CASServer::AuthenticatorError => e
361
+ $LOG.error(e)
362
+ @message = {:type => 'mistake', :message => e.to_s}
363
+ return render(:login)
364
+ end
365
+
366
+ if credentials_are_valid
367
+ $LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
368
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
369
+
370
+ # 3.6 (ticket-granting cookie)
371
+ tgt = generate_ticket_granting_ticket(@username, extra_attributes)
372
+
373
+ if settings.config[:maximum_session_lifetime]
374
+ expires = settings.config[:maximum_session_lifetime].to_i.from_now
375
+ expiry_info = " It will expire on #{expires}."
376
+
377
+ response.set_cookie('tgt', {
378
+ :value => tgt.to_s,
379
+ :expires => expires
380
+ })
381
+ else
382
+ expiry_info = " It will not expire."
383
+ response.set_cookie('tgt', tgt.to_s)
384
+ end
385
+
386
+ $LOG.debug("Ticket granting cookie '#{request.cookies['tgt'].inspect}' granted to #{@username.inspect}. #{expiry_info}")
387
+
388
+ if @service.blank?
389
+ $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
390
+ @message = {:type => 'confirmation', :message => _("You have successfully logged in.")}
391
+ else
392
+ @st = generate_service_ticket(@service, @username, tgt)
393
+
394
+ begin
395
+ service_with_ticket = service_uri_with_ticket(@service, @st)
396
+
397
+ $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
398
+ redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
399
+ rescue URI::InvalidURIError
400
+ $LOG.error("The service '#{@service}' is not a valid URI!")
401
+ @message = {
402
+ :type => 'mistake',
403
+ :message => _("The target service your browser supplied appears to be invalid. Please contact your system administrator for help.")
404
+ }
405
+ end
406
+ end
407
+ else
408
+ $LOG.warn("Invalid credentials given for user '#{@username}'")
409
+ @message = {:type => 'mistake', :message => _("Incorrect username or password.")}
410
+ status 401
411
+ end
412
+
413
+ render :erb, :login
414
+ end
415
+
416
+ get /^#{uri_path}\/?$/ do
417
+ redirect "#{config['uri_path']}/login", 303
418
+ end
419
+
420
+
421
+ # 2.3
422
+
423
+ # 2.3.1
424
+ get "#{uri_path}/logout" do
425
+ CASServer::Utils::log_controller_action(self.class, params)
426
+
427
+ # The behaviour here is somewhat non-standard. Rather than showing just a blank
428
+ # "logout" page, we take the user back to the login page with a "you have been logged out"
429
+ # message, allowing for an opportunity to immediately log back in. This makes it
430
+ # easier for the user to log out and log in as someone else.
431
+ @service = clean_service_url(params['service'] || params['destination'])
432
+ @continue_url = params['url']
433
+
434
+ @gateway = params['gateway'] == 'true' || params['gateway'] == '1'
435
+
436
+ tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(request.cookies['tgt'])
437
+
438
+ response.delete_cookie 'tgt'
439
+
440
+ if tgt
441
+ CASServer::Model::TicketGrantingTicket.transaction do
442
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
443
+ tgt.granted_service_tickets.each do |st|
444
+ send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
445
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
446
+ # (the above method returns false if the POST results in a non-200 HTTP response).
447
+ $LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
448
+ st.destroy
449
+ end
450
+
451
+ pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
452
+ :conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
453
+ :include => :service_ticket)
454
+ pgts.each do |pgt|
455
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
456
+ pgt.destroy
457
+ end
458
+
459
+ $LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
460
+ tgt.destroy
461
+ end
462
+
463
+ $LOG.info("User '#{tgt.username}' logged out.")
464
+ else
465
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
466
+ end
467
+
468
+ @message = {:type => 'confirmation', :message => _("You have successfully logged out.")}
469
+
470
+ @message[:message] +=_(" Please click on the following link to continue:") if @continue_url
471
+
472
+ @lt = generate_login_ticket
473
+
474
+ if @gateway && @service
475
+ redirect @service, 303
476
+ elsif @continue_url
477
+ render :erb, :logout
478
+ else
479
+ render :erb, :login
480
+ end
481
+ end
482
+
483
+
484
+ # 2.4
485
+
486
+ # 2.4.1
487
+ get "#{uri_path}/validate" do
488
+ CASServer::Utils::log_controller_action(self.class, params)
489
+
490
+ # required
491
+ @service = clean_service_url(params['service'])
492
+ @ticket = params['ticket']
493
+ # optional
494
+ @renew = params['renew']
495
+
496
+ st, @error = validate_service_ticket(@service, @ticket)
497
+ @success = st && !@error
498
+
499
+ @username = st.username if @success
500
+
501
+ status response_status_from_error(@error) if @error
502
+
503
+ render :erb, :validate, :layout => false
504
+ end
505
+
506
+
507
+ # 2.5
508
+
509
+ # 2.5.1
510
+ get "#{uri_path}/serviceValidate" do
511
+ CASServer::Utils::log_controller_action(self.class, params)
512
+
513
+ # required
514
+ @service = clean_service_url(params['service'])
515
+ @ticket = params['ticket']
516
+ # optional
517
+ @renew = params['renew']
518
+
519
+ st, @error = validate_service_ticket(@service, @ticket)
520
+ @success = st && !@error
521
+
522
+ if @success
523
+ @username = st.username
524
+ if @pgt_url
525
+ pgt = generate_proxy_granting_ticket(@pgt_url, st)
526
+ @pgtiou = pgt.iou if pgt
527
+ end
528
+ @extra_attributes = st.granted_by_tgt.extra_attributes || {}
529
+ end
530
+
531
+ status response_status_from_error(@error) if @error
532
+
533
+ render :builder, :proxy_validate
534
+ end
535
+
536
+
537
+ # 2.6
538
+
539
+ # 2.6.1
540
+ get "#{uri_path}/proxyValidate" do
541
+ CASServer::Utils::log_controller_action(self.class, params)
542
+
543
+ # required
544
+ @service = clean_service_url(params['service'])
545
+ @ticket = params['ticket']
546
+ # optional
547
+ @pgt_url = params['pgtUrl']
548
+ @renew = params['renew']
549
+
550
+ @proxies = []
551
+
552
+ t, @error = validate_proxy_ticket(@service, @ticket)
553
+ @success = t && !@error
554
+
555
+ @extra_attributes = {}
556
+ if @success
557
+ @username = t.username
558
+
559
+ if t.kind_of? CASServer::Model::ProxyTicket
560
+ @proxies << t.granted_by_pgt.service_ticket.service
561
+ end
562
+
563
+ if @pgt_url
564
+ pgt = generate_proxy_granting_ticket(@pgt_url, t)
565
+ @pgtiou = pgt.iou if pgt
566
+ end
567
+
568
+ @extra_attributes = t.granted_by_tgt.extra_attributes || {}
569
+ end
570
+
571
+ status response_status_from_error(@error) if @error
572
+
573
+ render :builder, :proxy_validate
574
+ end
575
+
576
+
577
+ # 2.7
578
+ get "#{uri_path}/proxy" do
579
+ CASServer::Utils::log_controller_action(self.class, params)
580
+
581
+ # required
582
+ @ticket = params['pgt']
583
+ @target_service = params['targetService']
584
+
585
+ pgt, @error = validate_proxy_granting_ticket(@ticket)
586
+ @success = pgt && !@error
587
+
588
+ if @success
589
+ @pt = generate_proxy_ticket(@target_service, pgt)
590
+ end
591
+
592
+ status response_status_from_error(@error) if @error
593
+
594
+ render :builder, :proxy
595
+ end
596
+
597
+
598
+
599
+ # Helpers
600
+
601
+ def response_status_from_error(error)
602
+ case error.code.to_s
603
+ when /^INVALID_/, 'BAD_PGT'
604
+ 422
605
+ when 'INTERNAL_ERROR'
606
+ 500
607
+ else
608
+ 500
609
+ end
610
+ end
611
+
612
+ def serialize_extra_attribute(builder, value)
613
+ if value.kind_of?(String)
614
+ builder.text! value
615
+ elsif value.kind_of?(Numeric)
616
+ builder.text! value.to_s
617
+ else
618
+ builder.cdata! value.to_yaml
619
+ end
620
+ end
621
+ end
622
+ end
623
+