synapses-cas 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG +3 -0
- data/Gemfile +3 -0
- data/LICENSE +27 -0
- data/README.md +20 -0
- data/Rakefile +2 -0
- data/bin/synapses-cas +30 -0
- data/config.ru +17 -0
- data/config/config.example.yml +592 -0
- data/config/unicorn.rb +88 -0
- data/db/migrate/001_create_initial_structure.rb +47 -0
- data/lib/casserver.rb +11 -0
- data/lib/casserver/authenticators/active_directory_ldap.rb +19 -0
- data/lib/casserver/authenticators/active_resource.rb +127 -0
- data/lib/casserver/authenticators/authlogic_crypto_providers/aes256.rb +43 -0
- data/lib/casserver/authenticators/authlogic_crypto_providers/bcrypt.rb +92 -0
- data/lib/casserver/authenticators/authlogic_crypto_providers/md5.rb +34 -0
- data/lib/casserver/authenticators/authlogic_crypto_providers/sha1.rb +59 -0
- data/lib/casserver/authenticators/authlogic_crypto_providers/sha512.rb +50 -0
- data/lib/casserver/authenticators/base.rb +67 -0
- data/lib/casserver/authenticators/client_certificate.rb +47 -0
- data/lib/casserver/authenticators/google.rb +58 -0
- data/lib/casserver/authenticators/ldap.rb +147 -0
- data/lib/casserver/authenticators/ntlm.rb +88 -0
- data/lib/casserver/authenticators/open_id.rb +22 -0
- data/lib/casserver/authenticators/sql.rb +133 -0
- data/lib/casserver/authenticators/sql_authlogic.rb +93 -0
- data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
- data/lib/casserver/authenticators/sql_md5.rb +19 -0
- data/lib/casserver/authenticators/sql_rest_auth.rb +82 -0
- data/lib/casserver/authenticators/test.rb +22 -0
- data/lib/casserver/cas.rb +323 -0
- data/lib/casserver/localization.rb +13 -0
- data/lib/casserver/model.rb +270 -0
- data/lib/casserver/server.rb +758 -0
- data/lib/casserver/utils.rb +32 -0
- data/lib/casserver/views/_login_form.erb +42 -0
- data/lib/casserver/views/layout.erb +18 -0
- data/lib/casserver/views/login.erb +30 -0
- data/lib/casserver/views/proxy.builder +12 -0
- data/lib/casserver/views/proxy_validate.builder +25 -0
- data/lib/casserver/views/service_validate.builder +18 -0
- data/lib/casserver/views/validate.erb +2 -0
- data/locales/de.yml +27 -0
- data/locales/en.yml +26 -0
- data/locales/es.yml +26 -0
- data/locales/es_ar.yml +26 -0
- data/locales/fr.yml +26 -0
- data/locales/jp.yml +26 -0
- data/locales/pl.yml +26 -0
- data/locales/pt.yml +26 -0
- data/locales/ru.yml +26 -0
- data/locales/zh.yml +26 -0
- data/locales/zh_tw.yml +26 -0
- data/public/themes/cas.css +126 -0
- data/public/themes/notice.png +0 -0
- data/public/themes/ok.png +0 -0
- data/public/themes/simple/bg.png +0 -0
- data/public/themes/simple/favicon.png +0 -0
- data/public/themes/simple/login_box_bg.png +0 -0
- data/public/themes/simple/logo.png +0 -0
- data/public/themes/simple/theme.css +28 -0
- data/public/themes/urbacon/bg.png +0 -0
- data/public/themes/urbacon/login_box_bg.png +0 -0
- data/public/themes/urbacon/logo.png +0 -0
- data/public/themes/urbacon/theme.css +33 -0
- data/public/themes/warning.png +0 -0
- data/resources/init.d.sh +58 -0
- data/setup.rb +1585 -0
- data/spec/alt_config.yml +50 -0
- data/spec/authenticators/active_resource_spec.rb +109 -0
- data/spec/authenticators/ldap_spec.rb +53 -0
- data/spec/casserver_spec.rb +156 -0
- data/spec/default_config.yml +50 -0
- data/spec/model_spec.rb +42 -0
- data/spec/spec.opts +4 -0
- data/spec/spec_helper.rb +89 -0
- data/spec/utils_spec.rb +53 -0
- data/tasks/bundler.rake +4 -0
- data/tasks/db/migrate.rake +12 -0
- data/tasks/spec.rake +10 -0
- metadata +380 -0
@@ -0,0 +1,758 @@
|
|
1
|
+
require 'sinatra/base'
|
2
|
+
require 'casserver/localization'
|
3
|
+
require 'casserver/utils'
|
4
|
+
require 'casserver/cas'
|
5
|
+
|
6
|
+
require 'logger'
|
7
|
+
$LOG ||= Logger.new(STDOUT)
|
8
|
+
|
9
|
+
module CASServer
|
10
|
+
class Server < Sinatra::Base
|
11
|
+
if ENV['CONFIG_FILE']
|
12
|
+
CONFIG_FILE = ENV['CONFIG_FILE']
|
13
|
+
elsif !(c_file = File.dirname(__FILE__) + "/../../config.yml").nil? && File.exist?(c_file)
|
14
|
+
CONFIG_FILE = c_file
|
15
|
+
else
|
16
|
+
CONFIG_FILE = "/etc/synapses-cas/config.yml"
|
17
|
+
end
|
18
|
+
|
19
|
+
include CASServer::CAS # CAS protocol helpers
|
20
|
+
include Localization
|
21
|
+
|
22
|
+
# Use :public_folder for Sinatra >= 1.3, and :public for older versions.
|
23
|
+
def self.use_public_folder?
|
24
|
+
Sinatra.const_defined?("VERSION") && Gem::Version.new(Sinatra::VERSION) >= Gem::Version.new("1.3.0")
|
25
|
+
end
|
26
|
+
|
27
|
+
set :app_file, __FILE__
|
28
|
+
set( use_public_folder? ? :public_folder : :public, # Workaround for differences in Sinatra versions.
|
29
|
+
Proc.new { settings.config[:public_dir] || File.join(root, "..", "..", "public") } )
|
30
|
+
|
31
|
+
config = HashWithIndifferentAccess.new(
|
32
|
+
:maximum_unused_login_ticket_lifetime => 5.minutes,
|
33
|
+
:maximum_unused_service_ticket_lifetime => 5.minutes, # CAS Protocol Spec, sec. 3.2.1 (recommended expiry time)
|
34
|
+
:maximum_session_lifetime => 2.days, # all tickets are deleted after this period of time
|
35
|
+
:log => {:file => 'casserver.log', :level => 'DEBUG'},
|
36
|
+
:uri_path => ""
|
37
|
+
)
|
38
|
+
set :config, config
|
39
|
+
|
40
|
+
def self.uri_path
|
41
|
+
config[:uri_path]
|
42
|
+
end
|
43
|
+
|
44
|
+
# Strip the config.uri_path from the request.path_info...
|
45
|
+
# FIXME: do we really need to override all of Sinatra's #static! to make this happen?
|
46
|
+
def static!
|
47
|
+
# Workaround for differences in Sinatra versions.
|
48
|
+
public_dir = Server.use_public_folder? ? settings.public_folder : settings.public
|
49
|
+
return if public_dir.nil?
|
50
|
+
public_dir = File.expand_path(public_dir)
|
51
|
+
|
52
|
+
path = File.expand_path(public_dir + unescape(request.path_info.gsub(/^#{settings.config[:uri_path]}/,'')))
|
53
|
+
return if path[0, public_dir.length] != public_dir
|
54
|
+
return unless File.file?(path)
|
55
|
+
|
56
|
+
env['sinatra.static_file'] = path
|
57
|
+
send_file path, :disposition => nil
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.run!(options={})
|
61
|
+
set options
|
62
|
+
|
63
|
+
handler = detect_rack_handler
|
64
|
+
handler_name = handler.name.gsub(/.*::/, '')
|
65
|
+
|
66
|
+
puts "== Synapses-CAS Server is starting up " +
|
67
|
+
"on port #{config[:port] || port} for #{environment} with backup from #{handler_name}" unless handler_name =~/cgi/i
|
68
|
+
|
69
|
+
begin
|
70
|
+
opts = handler_options
|
71
|
+
rescue Exception => e
|
72
|
+
print_cli_message e, :error
|
73
|
+
raise e
|
74
|
+
end
|
75
|
+
|
76
|
+
handler.run self, opts do |server|
|
77
|
+
[:INT, :TERM].each { |sig| trap(sig) { quit!(server, handler_name) } }
|
78
|
+
set :running, true
|
79
|
+
end
|
80
|
+
rescue Errno::EADDRINUSE => e
|
81
|
+
puts "== Something is already running on port #{port}!"
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.quit!(server, handler_name)
|
85
|
+
## Use thins' hard #stop! if available, otherwise just #stop
|
86
|
+
server.respond_to?(:stop!) ? server.stop! : server.stop
|
87
|
+
puts "\n== Synapses-CAS Server is shutting down" unless handler_name =~/cgi/i
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.print_cli_message(msg, type = :info)
|
91
|
+
if respond_to?(:config) && config && config[:quiet]
|
92
|
+
return
|
93
|
+
end
|
94
|
+
|
95
|
+
if type == :error
|
96
|
+
io = $stderr
|
97
|
+
prefix = "!!! "
|
98
|
+
else
|
99
|
+
io = $stdout
|
100
|
+
prefix = ">>> "
|
101
|
+
end
|
102
|
+
|
103
|
+
io.puts
|
104
|
+
io.puts "#{prefix}#{msg}"
|
105
|
+
io.puts
|
106
|
+
end
|
107
|
+
|
108
|
+
def self.load_config_file(config_file)
|
109
|
+
begin
|
110
|
+
config_file = File.open(config_file)
|
111
|
+
rescue Errno::ENOENT => e
|
112
|
+
|
113
|
+
print_cli_message "Config file #{config_file} does not exist!", :error
|
114
|
+
print_cli_message "Would you like the default config file copied to #{config_file.inspect}? [y/N]"
|
115
|
+
if gets.strip.downcase == 'y'
|
116
|
+
require 'fileutils'
|
117
|
+
default_config = File.dirname(__FILE__) + '/../../config/config.example.yml'
|
118
|
+
|
119
|
+
if !File.exists?(File.dirname(config_file))
|
120
|
+
print_cli_message "Creating config directory..."
|
121
|
+
FileUtils.mkdir_p(File.dirname(config_file), :verbose => true)
|
122
|
+
end
|
123
|
+
|
124
|
+
print_cli_message "Copying #{default_config.inspect} to #{config_file.inspect}..."
|
125
|
+
FileUtils.cp(default_config, config_file, :verbose => true)
|
126
|
+
print_cli_message "The default config has been copied. You should now edit it and try starting again."
|
127
|
+
exit
|
128
|
+
else
|
129
|
+
print_cli_message "Cannot start RubyCAS-Server without a valid config file.", :error
|
130
|
+
raise e
|
131
|
+
end
|
132
|
+
rescue Errno::EACCES => e
|
133
|
+
print_cli_message "Config file #{config_file.inspect} is not readable (permission denied)!", :error
|
134
|
+
raise e
|
135
|
+
rescue => e
|
136
|
+
print_cli_message "Config file #{config_file.inspect} could not be read!", :error
|
137
|
+
raise e
|
138
|
+
end
|
139
|
+
|
140
|
+
config.merge! HashWithIndifferentAccess.new(YAML.load(config_file))
|
141
|
+
set :server, config[:server] || 'webrick'
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.reconfigure!(config)
|
145
|
+
config.each do |key, val|
|
146
|
+
self.config[key] = val
|
147
|
+
end
|
148
|
+
init_database!
|
149
|
+
init_logger!
|
150
|
+
init_authenticators!
|
151
|
+
end
|
152
|
+
|
153
|
+
def self.handler_options
|
154
|
+
handler_options = {
|
155
|
+
:Host => bind || config[:bind_address],
|
156
|
+
:Port => config[:port] || 443
|
157
|
+
}
|
158
|
+
|
159
|
+
handler_options.merge(handler_ssl_options).to_hash.symbolize_keys!
|
160
|
+
end
|
161
|
+
|
162
|
+
def self.handler_ssl_options
|
163
|
+
return {} unless config[:ssl_cert]
|
164
|
+
|
165
|
+
cert_path = config[:ssl_cert]
|
166
|
+
key_path = config[:ssl_key] || config[:ssl_cert]
|
167
|
+
|
168
|
+
unless cert_path.nil? && key_path.nil?
|
169
|
+
raise "The ssl_cert and ssl_key options cannot be used with mongrel. You will have to run your " +
|
170
|
+
" server behind a reverse proxy if you want SSL under mongrel." if
|
171
|
+
config[:server] == 'mongrel'
|
172
|
+
|
173
|
+
raise "The specified certificate file #{cert_path.inspect} does not exist or is not readable. " +
|
174
|
+
" Your 'ssl_cert' configuration setting must be a path to a valid " +
|
175
|
+
" ssl certificate." unless
|
176
|
+
File.exists? cert_path
|
177
|
+
|
178
|
+
raise "The specified key file #{key_path.inspect} does not exist or is not readable. " +
|
179
|
+
" Your 'ssl_key' configuration setting must be a path to a valid " +
|
180
|
+
" ssl private key." unless
|
181
|
+
File.exists? key_path
|
182
|
+
|
183
|
+
require 'openssl'
|
184
|
+
require 'webrick/https'
|
185
|
+
|
186
|
+
cert = OpenSSL::X509::Certificate.new(File.read(cert_path))
|
187
|
+
key = OpenSSL::PKey::RSA.new(File.read(key_path))
|
188
|
+
|
189
|
+
{
|
190
|
+
:SSLEnable => true,
|
191
|
+
:SSLVerifyClient => ::OpenSSL::SSL::VERIFY_NONE,
|
192
|
+
:SSLCertificate => cert,
|
193
|
+
:SSLPrivateKey => key
|
194
|
+
}
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
def self.init_authenticators!
|
199
|
+
auth = []
|
200
|
+
|
201
|
+
if config[:authenticator].nil?
|
202
|
+
print_cli_message "No authenticators have been configured. Please double-check your config file (#{CONFIG_FILE.inspect}).", :error
|
203
|
+
exit 1
|
204
|
+
end
|
205
|
+
|
206
|
+
begin
|
207
|
+
# attempt to instantiate the authenticator
|
208
|
+
config[:authenticator] = [config[:authenticator]] unless config[:authenticator].instance_of? Array
|
209
|
+
config[:authenticator].each { |authenticator| auth << authenticator[:class].constantize}
|
210
|
+
rescue NameError
|
211
|
+
if config[:authenticator].instance_of? Array
|
212
|
+
config[:authenticator].each do |authenticator|
|
213
|
+
if !authenticator[:source].nil?
|
214
|
+
# config.yml explicitly names source file
|
215
|
+
require authenticator[:source]
|
216
|
+
else
|
217
|
+
# the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
|
218
|
+
auth_rb = authenticator[:class].underscore.gsub('cas_server/', '')
|
219
|
+
require 'casserver/'+auth_rb
|
220
|
+
end
|
221
|
+
auth << authenticator[:class].constantize
|
222
|
+
end
|
223
|
+
else
|
224
|
+
if config[:authenticator][:source]
|
225
|
+
# config.yml explicitly names source file
|
226
|
+
require config[:authenticator][:source]
|
227
|
+
else
|
228
|
+
# the authenticator class hasn't yet been loaded, so lets try to load it from the casserver/authenticators directory
|
229
|
+
auth_rb = config[:authenticator][:class].underscore.gsub('cas_server/', '')
|
230
|
+
require 'casserver/'+auth_rb
|
231
|
+
end
|
232
|
+
|
233
|
+
auth << config[:authenticator][:class].constantize
|
234
|
+
config[:authenticator] = [config[:authenticator]]
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
auth.zip(config[:authenticator]).each_with_index{ |auth_conf, index|
|
239
|
+
authenticator, conf = auth_conf
|
240
|
+
$LOG.debug "About to setup #{authenticator} with #{conf.inspect}..."
|
241
|
+
authenticator.setup(conf.merge('auth_index' => index)) if authenticator.respond_to?(:setup)
|
242
|
+
$LOG.debug "Done setting up #{authenticator}."
|
243
|
+
}
|
244
|
+
|
245
|
+
set :auth, auth
|
246
|
+
end
|
247
|
+
|
248
|
+
def self.init_logger!
|
249
|
+
if config[:log]
|
250
|
+
if $LOG && config[:log][:file]
|
251
|
+
print_cli_message "Redirecting RubyCAS-Server log to #{config[:log][:file]}"
|
252
|
+
#$LOG.close
|
253
|
+
$LOG = Logger.new(config[:log][:file])
|
254
|
+
end
|
255
|
+
$LOG.level = Logger.const_get(config[:log][:level]) if config[:log][:level]
|
256
|
+
end
|
257
|
+
|
258
|
+
if config[:db_log]
|
259
|
+
if $LOG && config[:db_log][:file]
|
260
|
+
$LOG.debug "Redirecting ActiveRecord log to #{config[:log][:file]}"
|
261
|
+
#$LOG.close
|
262
|
+
ActiveRecord::Base.logger = Logger.new(config[:db_log][:file])
|
263
|
+
end
|
264
|
+
ActiveRecord::Base.logger.level = Logger.const_get(config[:db_log][:level]) if config[:db_log][:level]
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
def self.init_database!
|
269
|
+
|
270
|
+
unless config[:disable_auto_migrations]
|
271
|
+
ActiveRecord::Base.establish_connection(config[:database])
|
272
|
+
print_cli_message "Running migrations to make sure your database schema is up to date..."
|
273
|
+
prev_db_log = ActiveRecord::Base.logger
|
274
|
+
ActiveRecord::Base.logger = Logger.new(STDOUT)
|
275
|
+
ActiveRecord::Migration.verbose = true
|
276
|
+
ActiveRecord::Migrator.migrate(File.dirname(__FILE__) + "/../../db/migrate")
|
277
|
+
ActiveRecord::Base.logger = prev_db_log
|
278
|
+
print_cli_message "Your database is now up to date."
|
279
|
+
end
|
280
|
+
|
281
|
+
ActiveRecord::Base.establish_connection(config[:database])
|
282
|
+
end
|
283
|
+
|
284
|
+
configure do
|
285
|
+
load_config_file(CONFIG_FILE)
|
286
|
+
init_logger!
|
287
|
+
init_database!
|
288
|
+
init_authenticators!
|
289
|
+
end
|
290
|
+
|
291
|
+
before do
|
292
|
+
content_type :html, 'charset' => 'utf-8'
|
293
|
+
@theme = settings.config[:theme]
|
294
|
+
@organization = settings.config[:organization]
|
295
|
+
@uri_path = settings.config[:uri_path]
|
296
|
+
@infoline = settings.config[:infoline]
|
297
|
+
@custom_views = settings.config[:custom_views]
|
298
|
+
@template_engine = settings.config[:template_engine] || :erb
|
299
|
+
if @template_engine != :erb
|
300
|
+
require @template_engine
|
301
|
+
@template_engine = @template_engine.to_sym
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
|
306
|
+
# under http://www.ja-sig.org/products/cas/overview/protocol/index.html
|
307
|
+
|
308
|
+
# 2.1 :: Login
|
309
|
+
|
310
|
+
# 2.1.1
|
311
|
+
get "#{uri_path}/login" do
|
312
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
313
|
+
|
314
|
+
# make sure there's no caching
|
315
|
+
headers['Pragma'] = 'no-cache'
|
316
|
+
headers['Cache-Control'] = 'no-store'
|
317
|
+
headers['Expires'] = (Time.now - 1.year).rfc2822
|
318
|
+
|
319
|
+
# optional params
|
320
|
+
@service = clean_service_url(params['service'])
|
321
|
+
@renew = params['renew']
|
322
|
+
@gateway = params['gateway'] == 'true' || params['gateway'] == '1'
|
323
|
+
|
324
|
+
if tgc = request.cookies['tgt']
|
325
|
+
tgt, tgt_error = validate_ticket_granting_ticket(tgc)
|
326
|
+
end
|
327
|
+
|
328
|
+
if tgt and !tgt_error
|
329
|
+
@message = {:type => 'notice',
|
330
|
+
:message => t.notice.logged_in_as(tgt.username)}
|
331
|
+
elsif tgt_error
|
332
|
+
$LOG.debug("Ticket granting cookie could not be validated: #{tgt_error}")
|
333
|
+
elsif !tgt
|
334
|
+
$LOG.debug("No ticket granting ticket detected.")
|
335
|
+
end
|
336
|
+
|
337
|
+
if params['redirection_loop_intercepted']
|
338
|
+
@message = {:type => 'mistake',
|
339
|
+
:message => t.error.unable_to_authenticate}
|
340
|
+
end
|
341
|
+
|
342
|
+
begin
|
343
|
+
if @service
|
344
|
+
if @renew
|
345
|
+
$LOG.info("Authentication renew explicitly requested. Proceeding with CAS login for service #{@service.inspect}.")
|
346
|
+
elsif tgt && !tgt_error
|
347
|
+
$LOG.debug("Valid ticket granting ticket detected.")
|
348
|
+
st = generate_service_ticket(@service, tgt.username, tgt)
|
349
|
+
service_with_ticket = service_uri_with_ticket(@service, st)
|
350
|
+
$LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
|
351
|
+
redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
|
352
|
+
elsif @gateway
|
353
|
+
$LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
|
354
|
+
redirect @service, 303
|
355
|
+
else
|
356
|
+
$LOG.info("Proceeding with CAS login for service #{@service.inspect}.")
|
357
|
+
end
|
358
|
+
elsif @gateway
|
359
|
+
$LOG.error("This is a gateway request but no service parameter was given!")
|
360
|
+
@message = {:type => 'mistake',
|
361
|
+
:message => t.error.no_service_parameter_given}
|
362
|
+
else
|
363
|
+
$LOG.info("Proceeding with CAS login without a target service.")
|
364
|
+
end
|
365
|
+
rescue URI::InvalidURIError
|
366
|
+
$LOG.error("The service '#{@service}' is not a valid URI!")
|
367
|
+
@message = {:type => 'mistake',
|
368
|
+
:message => t.error.invalid_target_service}
|
369
|
+
end
|
370
|
+
|
371
|
+
lt = generate_login_ticket
|
372
|
+
|
373
|
+
$LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
|
374
|
+
|
375
|
+
@lt = lt.ticket
|
376
|
+
|
377
|
+
#$LOG.debug(env)
|
378
|
+
|
379
|
+
# If the 'onlyLoginForm' parameter is specified, we will only return the
|
380
|
+
# login form part of the page. This is useful for when you want to
|
381
|
+
# embed the login form in some external page (as an IFRAME, or otherwise).
|
382
|
+
# The optional 'submitToURI' parameter can be given to explicitly set the
|
383
|
+
# action for the form, otherwise the server will try to guess this for you.
|
384
|
+
if params.has_key? 'onlyLoginForm'
|
385
|
+
if @env['HTTP_HOST']
|
386
|
+
guessed_login_uri = "http#{@env['HTTPS'] && @env['HTTPS'] == 'on' ? 's' : ''}://#{@env['REQUEST_URI']}#{self / '/login'}"
|
387
|
+
else
|
388
|
+
guessed_login_uri = nil
|
389
|
+
end
|
390
|
+
|
391
|
+
@form_action = params['submitToURI'] || guessed_login_uri
|
392
|
+
|
393
|
+
if @form_action
|
394
|
+
render :login_form
|
395
|
+
else
|
396
|
+
status 500
|
397
|
+
render t.error.invalid_submit_to_uri
|
398
|
+
end
|
399
|
+
else
|
400
|
+
render @template_engine, :login
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
|
405
|
+
# 2.2
|
406
|
+
post "#{uri_path}/login" do
|
407
|
+
Utils::log_controller_action(self.class, params)
|
408
|
+
|
409
|
+
# 2.2.1 (optional)
|
410
|
+
@service = clean_service_url(params['service'])
|
411
|
+
|
412
|
+
# 2.2.2 (required)
|
413
|
+
@username = params['username']
|
414
|
+
@password = params['password']
|
415
|
+
@lt = params['lt']
|
416
|
+
|
417
|
+
# Remove leading and trailing widespace from username.
|
418
|
+
@username.strip! if @username
|
419
|
+
|
420
|
+
if @username && settings.config[:downcase_username]
|
421
|
+
$LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
|
422
|
+
@username.downcase!
|
423
|
+
end
|
424
|
+
|
425
|
+
if error = validate_login_ticket(@lt)
|
426
|
+
@message = {:type => 'mistake', :message => error}
|
427
|
+
# generate another login ticket to allow for re-submitting the form
|
428
|
+
@lt = generate_login_ticket.ticket
|
429
|
+
status 500
|
430
|
+
return render @template_engine, :login
|
431
|
+
end
|
432
|
+
|
433
|
+
# generate another login ticket to allow for re-submitting the form after a post
|
434
|
+
@lt = generate_login_ticket.ticket
|
435
|
+
|
436
|
+
$LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{settings.auth.inspect}")
|
437
|
+
|
438
|
+
credentials_are_valid = false
|
439
|
+
extra_attributes = {}
|
440
|
+
successful_authenticator = nil
|
441
|
+
begin
|
442
|
+
auth_index = 0
|
443
|
+
settings.auth.each do |auth_class|
|
444
|
+
auth = auth_class.new
|
445
|
+
|
446
|
+
auth_config = settings.config[:authenticator][auth_index]
|
447
|
+
# pass the authenticator index to the configuration hash in case the authenticator needs to know
|
448
|
+
# it splace in the authenticator queue
|
449
|
+
auth.configure(auth_config.merge('auth_index' => auth_index))
|
450
|
+
|
451
|
+
credentials_are_valid = auth.validate(
|
452
|
+
:username => @username,
|
453
|
+
:password => @password,
|
454
|
+
:service => @service,
|
455
|
+
:request => @env
|
456
|
+
)
|
457
|
+
if credentials_are_valid
|
458
|
+
extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
|
459
|
+
successful_authenticator = auth
|
460
|
+
break
|
461
|
+
end
|
462
|
+
|
463
|
+
auth_index += 1
|
464
|
+
end
|
465
|
+
|
466
|
+
if credentials_are_valid
|
467
|
+
$LOG.info("Credentials for username '#{@username}' successfully validated using #{successful_authenticator.class.name}.")
|
468
|
+
$LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
|
469
|
+
|
470
|
+
# 3.6 (ticket-granting cookie)
|
471
|
+
tgt = generate_ticket_granting_ticket(@username, extra_attributes)
|
472
|
+
response.set_cookie('tgt', tgt.to_s)
|
473
|
+
|
474
|
+
$LOG.debug("Ticket granting cookie '#{tgt.inspect}' granted to #{@username.inspect}")
|
475
|
+
|
476
|
+
if @service.blank?
|
477
|
+
$LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
|
478
|
+
@message = {:type => 'confirmation', :message => t.notice.success_logged_in}
|
479
|
+
else
|
480
|
+
@st = generate_service_ticket(@service, @username, tgt)
|
481
|
+
|
482
|
+
begin
|
483
|
+
service_with_ticket = service_uri_with_ticket(@service, @st)
|
484
|
+
|
485
|
+
$LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
|
486
|
+
redirect service_with_ticket, 303 # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
|
487
|
+
rescue URI::InvalidURIError
|
488
|
+
$LOG.error("The service '#{@service}' is not a valid URI!")
|
489
|
+
@message = {
|
490
|
+
:type => 'mistake',
|
491
|
+
:message => t.error.invalid_target_service
|
492
|
+
}
|
493
|
+
end
|
494
|
+
end
|
495
|
+
else
|
496
|
+
$LOG.warn("Invalid credentials given for user '#{@username}'")
|
497
|
+
@message = {:type => 'mistake', :message => t.error.incorrect_username_or_password}
|
498
|
+
status 401
|
499
|
+
end
|
500
|
+
rescue CASServer::AuthenticatorError => e
|
501
|
+
$LOG.error(e)
|
502
|
+
# generate another login ticket to allow for re-submitting the form
|
503
|
+
@lt = generate_login_ticket.ticket
|
504
|
+
@message = {:type => 'mistake', :message => e.to_s}
|
505
|
+
status 401
|
506
|
+
end
|
507
|
+
|
508
|
+
render @template_engine, :login
|
509
|
+
end
|
510
|
+
|
511
|
+
get /^#{uri_path}\/?$/ do
|
512
|
+
redirect "#{config['uri_path']}/login", 303
|
513
|
+
end
|
514
|
+
|
515
|
+
|
516
|
+
# 2.3
|
517
|
+
|
518
|
+
# 2.3.1
|
519
|
+
get "#{uri_path}/logout" do
|
520
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
521
|
+
|
522
|
+
# The behaviour here is somewhat non-standard. Rather than showing just a blank
|
523
|
+
# "logout" page, we take the user back to the login page with a "you have been logged out"
|
524
|
+
# message, allowing for an opportunity to immediately log back in. This makes it
|
525
|
+
# easier for the user to log out and log in as someone else.
|
526
|
+
@service = clean_service_url(params['service'] || params['destination'])
|
527
|
+
@continue_url = params['url']
|
528
|
+
|
529
|
+
@gateway = params['gateway'] == 'true' || params['gateway'] == '1'
|
530
|
+
|
531
|
+
tgt = CASServer::Model::TicketGrantingTicket.find_by_ticket(request.cookies['tgt'])
|
532
|
+
|
533
|
+
response.delete_cookie 'tgt'
|
534
|
+
|
535
|
+
if tgt
|
536
|
+
CASServer::Model::TicketGrantingTicket.transaction do
|
537
|
+
$LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
|
538
|
+
tgt.granted_service_tickets.each do |st|
|
539
|
+
send_logout_notification_for_service_ticket(st) if config[:enable_single_sign_out]
|
540
|
+
# TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
|
541
|
+
# (the above method returns false if the POST results in a non-200 HTTP response).
|
542
|
+
$LOG.debug "Deleting #{st.class.name.demodulize} #{st.ticket.inspect} for service #{st.service}."
|
543
|
+
st.destroy
|
544
|
+
end
|
545
|
+
|
546
|
+
pgts = CASServer::Model::ProxyGrantingTicket.find(:all,
|
547
|
+
:conditions => [CASServer::Model::Base.connection.quote_table_name(CASServer::Model::ServiceTicket.table_name)+".username = ?", tgt.username],
|
548
|
+
:include => :service_ticket)
|
549
|
+
pgts.each do |pgt|
|
550
|
+
$LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
|
551
|
+
pgt.destroy
|
552
|
+
end
|
553
|
+
|
554
|
+
$LOG.debug("Deleting #{tgt.class.name.demodulize} '#{tgt}' for user '#{tgt.username}'")
|
555
|
+
tgt.destroy
|
556
|
+
end
|
557
|
+
|
558
|
+
$LOG.info("User '#{tgt.username}' logged out.")
|
559
|
+
else
|
560
|
+
$LOG.warn("User tried to log out without a valid ticket-granting ticket.")
|
561
|
+
end
|
562
|
+
|
563
|
+
@message = {:type => 'confirmation', :message => t.notice.success_logged_out}
|
564
|
+
|
565
|
+
@message[:message] += t.notice.click_to_continue if @continue_url
|
566
|
+
|
567
|
+
@lt = generate_login_ticket
|
568
|
+
|
569
|
+
if @gateway && @service
|
570
|
+
redirect @service, 303
|
571
|
+
elsif @continue_url
|
572
|
+
render @template_engine, :logout
|
573
|
+
else
|
574
|
+
render @template_engine, :login
|
575
|
+
end
|
576
|
+
end
|
577
|
+
|
578
|
+
|
579
|
+
# Handler for obtaining login tickets.
|
580
|
+
# This is useful when you want to build a custom login form located on a
|
581
|
+
# remote server. Your form will have to include a valid login ticket
|
582
|
+
# value, and this can be fetched from the CAS server using the POST handler.
|
583
|
+
|
584
|
+
get "#{uri_path}/loginTicket" do
|
585
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
586
|
+
|
587
|
+
$LOG.error("Tried to use login ticket dispenser with get method!")
|
588
|
+
|
589
|
+
status 422
|
590
|
+
|
591
|
+
t.error.login_ticket_needs_post_request
|
592
|
+
end
|
593
|
+
|
594
|
+
|
595
|
+
# Renders a page with a login ticket (and only the login ticket)
|
596
|
+
# in the response body.
|
597
|
+
post "#{uri_path}/loginTicket" do
|
598
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
599
|
+
|
600
|
+
lt = generate_login_ticket
|
601
|
+
|
602
|
+
$LOG.debug("Dispensing login ticket #{lt} to host #{(@env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']).inspect}")
|
603
|
+
|
604
|
+
@lt = lt.ticket
|
605
|
+
|
606
|
+
@lt
|
607
|
+
end
|
608
|
+
|
609
|
+
|
610
|
+
# 2.4
|
611
|
+
|
612
|
+
# 2.4.1
|
613
|
+
get "#{uri_path}/validate" do
|
614
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
615
|
+
|
616
|
+
# required
|
617
|
+
@service = clean_service_url(params['service'])
|
618
|
+
@ticket = params['ticket']
|
619
|
+
# optional
|
620
|
+
@renew = params['renew']
|
621
|
+
|
622
|
+
st, @error = validate_service_ticket(@service, @ticket)
|
623
|
+
@success = st && !@error
|
624
|
+
|
625
|
+
@username = st.username if @success
|
626
|
+
|
627
|
+
status response_status_from_error(@error) if @error
|
628
|
+
|
629
|
+
render @template_engine, :validate, :layout => false
|
630
|
+
end
|
631
|
+
|
632
|
+
|
633
|
+
# 2.5
|
634
|
+
|
635
|
+
# 2.5.1
|
636
|
+
get "#{uri_path}/serviceValidate" do
|
637
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
638
|
+
|
639
|
+
# required
|
640
|
+
@service = clean_service_url(params['service'])
|
641
|
+
@ticket = params['ticket']
|
642
|
+
# optional
|
643
|
+
@pgt_url = params['pgtUrl']
|
644
|
+
@renew = params['renew']
|
645
|
+
|
646
|
+
st, @error = validate_service_ticket(@service, @ticket)
|
647
|
+
@success = st && !@error
|
648
|
+
|
649
|
+
if @success
|
650
|
+
@username = st.username
|
651
|
+
if @pgt_url
|
652
|
+
pgt = generate_proxy_granting_ticket(@pgt_url, st)
|
653
|
+
@pgtiou = pgt.iou if pgt
|
654
|
+
end
|
655
|
+
@extra_attributes = st.granted_by_tgt.extra_attributes || {}
|
656
|
+
end
|
657
|
+
|
658
|
+
status response_status_from_error(@error) if @error
|
659
|
+
|
660
|
+
render :builder, :proxy_validate
|
661
|
+
end
|
662
|
+
|
663
|
+
|
664
|
+
# 2.6
|
665
|
+
|
666
|
+
# 2.6.1
|
667
|
+
get "#{uri_path}/proxyValidate" do
|
668
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
669
|
+
|
670
|
+
# required
|
671
|
+
@service = clean_service_url(params['service'])
|
672
|
+
@ticket = params['ticket']
|
673
|
+
# optional
|
674
|
+
@pgt_url = params['pgtUrl']
|
675
|
+
@renew = params['renew']
|
676
|
+
|
677
|
+
@proxies = []
|
678
|
+
|
679
|
+
t, @error = validate_proxy_ticket(@service, @ticket)
|
680
|
+
@success = t && !@error
|
681
|
+
|
682
|
+
@extra_attributes = {}
|
683
|
+
if @success
|
684
|
+
@username = t.username
|
685
|
+
|
686
|
+
if t.kind_of? CASServer::Model::ProxyTicket
|
687
|
+
@proxies << t.granted_by_pgt.service_ticket.service
|
688
|
+
end
|
689
|
+
|
690
|
+
if @pgt_url
|
691
|
+
pgt = generate_proxy_granting_ticket(@pgt_url, t)
|
692
|
+
@pgtiou = pgt.iou if pgt
|
693
|
+
end
|
694
|
+
|
695
|
+
@extra_attributes = t.granted_by_tgt.extra_attributes || {}
|
696
|
+
end
|
697
|
+
|
698
|
+
status response_status_from_error(@error) if @error
|
699
|
+
|
700
|
+
render :builder, :proxy_validate
|
701
|
+
end
|
702
|
+
|
703
|
+
|
704
|
+
# 2.7
|
705
|
+
get "#{uri_path}/proxy" do
|
706
|
+
CASServer::Utils::log_controller_action(self.class, params)
|
707
|
+
|
708
|
+
# required
|
709
|
+
@ticket = params['pgt']
|
710
|
+
@target_service = params['targetService']
|
711
|
+
|
712
|
+
pgt, @error = validate_proxy_granting_ticket(@ticket)
|
713
|
+
@success = pgt && !@error
|
714
|
+
|
715
|
+
if @success
|
716
|
+
@pt = generate_proxy_ticket(@target_service, pgt)
|
717
|
+
end
|
718
|
+
|
719
|
+
status response_status_from_error(@error) if @error
|
720
|
+
|
721
|
+
render :builder, :proxy
|
722
|
+
end
|
723
|
+
|
724
|
+
|
725
|
+
|
726
|
+
# Helpers
|
727
|
+
|
728
|
+
def response_status_from_error(error)
|
729
|
+
case error.code.to_s
|
730
|
+
when /^INVALID_/, 'BAD_PGT'
|
731
|
+
422
|
732
|
+
when 'INTERNAL_ERROR'
|
733
|
+
500
|
734
|
+
else
|
735
|
+
500
|
736
|
+
end
|
737
|
+
end
|
738
|
+
|
739
|
+
def serialize_extra_attribute(builder, key, value)
|
740
|
+
if value.kind_of?(String)
|
741
|
+
builder.tag! key, value
|
742
|
+
elsif value.kind_of?(Numeric)
|
743
|
+
builder.tag! key, value.to_s
|
744
|
+
else
|
745
|
+
builder.tag! key do
|
746
|
+
builder.cdata! value.to_yaml
|
747
|
+
end
|
748
|
+
end
|
749
|
+
end
|
750
|
+
|
751
|
+
def compile_template(engine, data, options, views)
|
752
|
+
super engine, data, options, @custom_views || views
|
753
|
+
rescue Errno::ENOENT
|
754
|
+
raise unless @custom_views
|
755
|
+
super engine, data, options, views
|
756
|
+
end
|
757
|
+
end
|
758
|
+
end
|