relevance-rubycas-server 0.6.99

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 (58) hide show
  1. data/.loadpath +5 -0
  2. data/.project +17 -0
  3. data/CHANGELOG.txt +1 -0
  4. data/History.txt +223 -0
  5. data/LICENSE.txt +504 -0
  6. data/Manifest.txt +61 -0
  7. data/README.txt +25 -0
  8. data/Rakefile +60 -0
  9. data/bin/rubycas-server +26 -0
  10. data/bin/rubycas-server-ctl +22 -0
  11. data/config.example.yml +363 -0
  12. data/custom_views.example.rb +11 -0
  13. data/lib/casserver.rb +110 -0
  14. data/lib/casserver/authenticators/active_directory_ldap.rb +11 -0
  15. data/lib/casserver/authenticators/base.rb +47 -0
  16. data/lib/casserver/authenticators/ldap.rb +108 -0
  17. data/lib/casserver/authenticators/ntlm.rb +88 -0
  18. data/lib/casserver/authenticators/sql.rb +102 -0
  19. data/lib/casserver/authenticators/sql_encrypted.rb +75 -0
  20. data/lib/casserver/authenticators/test.rb +15 -0
  21. data/lib/casserver/cas.rb +307 -0
  22. data/lib/casserver/conf.rb +112 -0
  23. data/lib/casserver/controllers.rb +436 -0
  24. data/lib/casserver/environment.rb +23 -0
  25. data/lib/casserver/models.rb +218 -0
  26. data/lib/casserver/postambles.rb +174 -0
  27. data/lib/casserver/utils.rb +30 -0
  28. data/lib/casserver/version.rb +9 -0
  29. data/lib/casserver/views.rb +235 -0
  30. data/lib/rubycas-server.rb +1 -0
  31. data/lib/rubycas-server/version.rb +1 -0
  32. data/lib/themes/cas.css +121 -0
  33. data/lib/themes/notice.png +0 -0
  34. data/lib/themes/ok.png +0 -0
  35. data/lib/themes/simple/bg.png +0 -0
  36. data/lib/themes/simple/login_box_bg.png +0 -0
  37. data/lib/themes/simple/logo.png +0 -0
  38. data/lib/themes/simple/theme.css +28 -0
  39. data/lib/themes/urbacon/bg.png +0 -0
  40. data/lib/themes/urbacon/login_box_bg.png +0 -0
  41. data/lib/themes/urbacon/logo.png +0 -0
  42. data/lib/themes/urbacon/theme.css +33 -0
  43. data/lib/themes/warning.png +0 -0
  44. data/misc/basic_cas_single_signon_mechanism_diagram.png +0 -0
  45. data/misc/basic_cas_single_signon_mechanism_diagram.svg +652 -0
  46. data/resources/init.d.sh +58 -0
  47. data/setup.rb +1585 -0
  48. data/test/test_cas.rb +33 -0
  49. data/test/test_casserver.rb +125 -0
  50. data/vendor/isaac_0.9.1/LICENSE +26 -0
  51. data/vendor/isaac_0.9.1/README +78 -0
  52. data/vendor/isaac_0.9.1/TODO +3 -0
  53. data/vendor/isaac_0.9.1/VERSIONS +3 -0
  54. data/vendor/isaac_0.9.1/crypt/ISAAC.rb +171 -0
  55. data/vendor/isaac_0.9.1/isaac.gemspec +39 -0
  56. data/vendor/isaac_0.9.1/setup.rb +596 -0
  57. data/vendor/isaac_0.9.1/test/TC_ISAAC.rb +76 -0
  58. metadata +158 -0
@@ -0,0 +1,436 @@
1
+ # The #.#.# comments (e.g. "2.1.3") refer to section numbers in the CAS protocol spec
2
+ # under http://www.ja-sig.org/products/cas/overview/protocol/index.html
3
+
4
+ module CASServer::Controllers
5
+
6
+ # 2.1
7
+ class Login < R '/', '/login'
8
+ include CASServer::CAS
9
+
10
+ # 2.1.1
11
+ def get
12
+ CASServer::Utils::log_controller_action(self.class, @input)
13
+
14
+ # make sure there's no caching
15
+ headers['Pragma'] = 'no-cache'
16
+ headers['Cache-Control'] = 'no-store'
17
+ headers['Expires'] = (Time.now - 1.year).rfc2822
18
+
19
+ # optional params
20
+ @service = clean_service_url(@input['service'])
21
+ @renew = @input['renew']
22
+ @gateway = @input['gateway'] == 'true' || @input['gateway'] == '1'
23
+
24
+ if tgc = @cookies[:tgt]
25
+ tgt, tgt_error = validate_ticket_granting_ticket(tgc)
26
+ end
27
+
28
+ if tgt and !tgt_error
29
+ @message = {:type => 'notice',
30
+ :message => %{You are currently logged in as "#{tgt.username}". If this is not you, please log in below.}}
31
+ end
32
+
33
+ if @input['redirection_loop_intercepted']
34
+ @message = {:type => 'mistake',
35
+ :message => %{The client and server are unable to negotiate authentication. Please try logging in again later.}}
36
+ end
37
+
38
+ begin
39
+ if @service
40
+ if !@renew && tgt && !tgt_error
41
+ st = generate_service_ticket(@service, tgt.username, tgt)
42
+ service_with_ticket = service_uri_with_ticket(@service, st)
43
+ $LOG.info("User '#{tgt.username}' authenticated based on ticket granting cookie. Redirecting to service '#{@service}'.")
44
+ return redirect(service_with_ticket, :status => 303) # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
45
+ elsif @gateway
46
+ $LOG.info("Redirecting unauthenticated gateway request to service '#{@service}'.")
47
+ return redirect(@service, :status => 303)
48
+ end
49
+ elsif @gateway
50
+ $LOG.error("This is a gateway request but no service parameter was given!")
51
+ @message = {:type => 'mistake',
52
+ :message => "The server cannot fulfill this gateway request because no service parameter was given."}
53
+ end
54
+ rescue URI::InvalidURIError
55
+ $LOG.error("The service '#{@service}' is not a valid URI!")
56
+ @message = {:type => 'mistake',
57
+ :message => "The target service your browser supplied appears to be invalid. Please contact your system administrator for help."}
58
+ end
59
+
60
+ lt = generate_login_ticket
61
+
62
+ $LOG.debug("Rendering login form with lt: #{lt}, service: #{@service}, renew: #{@renew}, gateway: #{@gateway}")
63
+
64
+ @lt = lt.ticket
65
+
66
+ #$LOG.debug(env)
67
+
68
+ # If the 'onlyLoginForm' parameter is specified, we will only return the
69
+ # login form part of the page. This is useful for when you want to
70
+ # embed the login form in some external page (as an IFRAME, or otherwise).
71
+ # The optional 'submitToURI' parameter can be given to explicitly set the
72
+ # action for the form, otherwise the server will try to guess this for you.
73
+ if @input.has_key? 'onlyLoginForm'
74
+ if env['HTTP_HOST']
75
+ guessed_login_uri = "http#{env['HTTPS'] && env['HTTPS'] == 'on' ? 's' : ''}://#{env['REQUEST_URI']}#{self / '/login'}"
76
+ else
77
+ guessed_login_uri = nil
78
+ end
79
+
80
+ @form_action = @input['submitToURI'] || guessed_login_uri
81
+
82
+ if @form_action
83
+ render :login_form
84
+ else
85
+ @status = 500
86
+ "Could not guess the CAS login URI. Please supply a submitURI parameter with your request."
87
+ end
88
+ else
89
+ render :login
90
+ end
91
+ end
92
+
93
+ # 2.2
94
+ def post
95
+ CASServer::Utils::log_controller_action(self.class, @input)
96
+
97
+ # 2.2.1 (optional)
98
+ @service = clean_service_url(@input['service'])
99
+
100
+ # FIXME: This is a potential security/phishing hole! Maybe @warn should
101
+ # be the id of a registered error string, rather than blindly
102
+ # printing whatever message the client wants printed... although
103
+ # I can't figure out if this is even used at all anymore.
104
+ @warn = @input['warn']
105
+
106
+ # 2.2.2 (required)
107
+ @username = @input['username']
108
+ @password = @input['password']
109
+ @lt = @input['lt']
110
+
111
+ # Remove leading and trailing widespace from username.
112
+ @username.strip! if @username
113
+
114
+ if @username && $CONF[:downcase_username]
115
+ $LOG.debug("Converting username #{@username.inspect} to lowercase because 'downcase_username' option is enabled.")
116
+ @username.downcase!
117
+ end
118
+
119
+ if error = validate_login_ticket(@lt)
120
+ @message = {:type => 'mistake', :message => error}
121
+ # generate another login ticket to allow for re-submitting the form
122
+ @lt = generate_login_ticket.ticket
123
+ @status = 401
124
+ return render(:login)
125
+ end
126
+
127
+ # generate another login ticket to allow for re-submitting the form after a post
128
+ @lt = generate_login_ticket.ticket
129
+
130
+ if $CONF[:authenticator].instance_of? Array
131
+ $AUTH.each_index {|auth_index| $AUTH[auth_index].configure(CASServer::Conf.authenticator[auth_index])}
132
+ else
133
+ $AUTH[0].configure(CASServer::Conf.authenticator)
134
+ end
135
+
136
+ $LOG.debug("Logging in with username: #{@username}, lt: #{@lt}, service: #{@service}, auth: #{$AUTH}")
137
+
138
+ credentials_are_valid = false
139
+ extra_attributes = {}
140
+ begin
141
+ $AUTH.each do |auth|
142
+ credentials_are_valid = auth.validate(
143
+ :username => @username,
144
+ :password => @password,
145
+ :service => @service,
146
+ :request => env
147
+ )
148
+ if credentials_are_valid
149
+ extra_attributes.merge!(auth.extra_attributes) unless auth.extra_attributes.blank?
150
+ break
151
+ end
152
+ end
153
+ rescue CASServer::AuthenticatorError => e
154
+ $LOG.error(e)
155
+ @message = {:type => 'mistake', :message => e.to_s}
156
+ return render(:login)
157
+ end
158
+
159
+ if credentials_are_valid
160
+ $LOG.info("Credentials for username '#{@username}' successfully validated")
161
+ $LOG.debug("Authenticator provided additional user attributes: #{extra_attributes.inspect}") unless extra_attributes.blank?
162
+
163
+ # 3.6 (ticket-granting cookie)
164
+ tgt = generate_ticket_granting_ticket(@username, extra_attributes)
165
+
166
+ if CASServer::Conf.expire_sessions
167
+ expires = CASServer::Conf.ticket_granting_ticket_expiry.to_i.from_now
168
+ expiry_info = " It will expire on #{expires}."
169
+ else
170
+ expiry_info = " It will not expire."
171
+ end
172
+
173
+ if CASServer::Conf.expire_sessions
174
+ @cookies[:tgt] = {
175
+ :value => tgt.to_s,
176
+ :expires => Time.now + CASServer::Conf.ticket_granting_ticket_expiry
177
+ }
178
+ else
179
+ @cookies[:tgt] = tgt.to_s
180
+ end
181
+
182
+ $LOG.debug("Ticket granting cookie '#{@cookies[:tgt].inspect}' granted to '#{@username.inspect}'. #{expiry_info}")
183
+
184
+ if @service.blank?
185
+ $LOG.info("Successfully authenticated user '#{@username}' at '#{tgt.client_hostname}'. No service param was given, so we will not redirect.")
186
+ @message = {:type => 'confirmation', :message => "You have successfully logged in."}
187
+ else
188
+ @st = generate_service_ticket(@service, @username, tgt)
189
+ begin
190
+ service_with_ticket = service_uri_with_ticket(@service, @st)
191
+
192
+ $LOG.info("Redirecting authenticated user '#{@username}' at '#{@st.client_hostname}' to service '#{@service}'")
193
+ return redirect(service_with_ticket, :status => 303) # response code 303 means "See Other" (see Appendix B in CAS Protocol spec)
194
+ rescue URI::InvalidURIError
195
+ $LOG.error("The service '#{@service}' is not a valid URI!")
196
+ @message = {:type => 'mistake', :message => "The target service your browser supplied appears to be invalid. Please contact your system administrator for help."}
197
+ end
198
+ end
199
+ else
200
+ $LOG.warn("Invalid credentials given for user '#{@username}'")
201
+ @message = {:type => 'mistake', :message => "Incorrect username or password."}
202
+ @status = 401
203
+ end
204
+
205
+ render :login
206
+ end
207
+ end
208
+
209
+ # 2.3
210
+ class Logout < R '/logout'
211
+ include CASServer::CAS
212
+
213
+ # 2.3.1
214
+ def get
215
+ CASServer::Utils::log_controller_action(self.class, @input)
216
+
217
+ # The behaviour here is somewhat non-standard. Rather than showing just a blank
218
+ # "logout" page, we take the user back to the login page with a "you have been logged out"
219
+ # message, allowing for an opportunity to immediately log back in. This makes it
220
+ # easier for the user to log out and log in as someone else.
221
+ @service = clean_service_url(@input['service'] || @input['destination'])
222
+ @continue_url = @input['url']
223
+
224
+ @gateway = @input['gateway'] == 'true' || @input['gateway'] == '1'
225
+
226
+ tgt = CASServer::Models::TicketGrantingTicket.find_by_ticket(@cookies[:tgt])
227
+
228
+ @cookies.delete :tgt
229
+
230
+ if tgt
231
+ CASServer::Models::TicketGrantingTicket.transaction do
232
+ pgts = CASServer::Models::ProxyGrantingTicket.find(:all,
233
+ :conditions => [CASServer::Models::Base.connection.quote_table_name(CASServer::Models::ServiceTicket.table_name)+".username = ?", tgt.username],
234
+ :include => :service_ticket)
235
+ pgts.each do |pgt|
236
+ $LOG.debug("Deleting Proxy-Granting Ticket '#{pgt}' for user '#{pgt.service_ticket.username}'")
237
+ pgt.destroy
238
+ end
239
+
240
+ if CASServer::Conf.enable_single_sign_out
241
+ $LOG.debug("Deleting Service/Proxy Tickets for '#{tgt}' for user '#{tgt.username}'")
242
+ tgt.service_tickets.each do |st|
243
+ send_logout_notification_for_service_ticket(st)
244
+ # TODO: Maybe we should do some special handling if send_logout_notification_for_service_ticket fails?
245
+ # Note that the method returns false if the POST results in a non-200 HTTP response.
246
+ $LOG.debug "Deleting #{st.class} #{st.ticket.inspect}."
247
+ st.destroy
248
+ end
249
+ end
250
+
251
+ $LOG.debug("Deleting Ticket-Granting Ticket '#{tgt}' for user '#{tgt.username}'")
252
+ tgt.destroy
253
+ end
254
+
255
+ $LOG.info("User '#{tgt.username}' logged out.")
256
+ else
257
+ $LOG.warn("User tried to log out without a valid ticket-granting ticket.")
258
+ end
259
+
260
+ @message = {:type => 'confirmation', :message => "You have successfully logged out."}
261
+
262
+ @message[:message] <<
263
+ " Please click on the following link to continue:" if @continue_url
264
+
265
+ @lt = generate_login_ticket
266
+
267
+ if @gateway && @service
268
+ redirect(@service, :status => 303)
269
+ elsif @continue_url
270
+ render :logout
271
+ else
272
+ render :login
273
+ end
274
+ end
275
+ end
276
+
277
+ # 2.4
278
+ class Validate < R '/validate'
279
+ include CASServer::CAS
280
+
281
+ # 2.4.1
282
+ def get
283
+ CASServer::Utils::log_controller_action(self.class, @input)
284
+
285
+ # required
286
+ @service = clean_service_url(@input['service'])
287
+ @ticket = @input['ticket']
288
+ # optional
289
+ @renew = @input['renew']
290
+
291
+ st, @error = validate_service_ticket(@service, @ticket)
292
+ @success = st && !@error
293
+
294
+ @username = st.username if @success
295
+
296
+ render :validate
297
+ end
298
+ end
299
+
300
+ # 2.5
301
+ class ServiceValidate < R '/serviceValidate'
302
+ include CASServer::CAS
303
+
304
+ # 2.5.1
305
+ def get
306
+ CASServer::Utils::log_controller_action(self.class, @input)
307
+
308
+ # required
309
+ @service = clean_service_url(@input['service'])
310
+ @ticket = @input['ticket']
311
+ # optional
312
+ @pgt_url = @input['pgtUrl']
313
+ @renew = @input['renew']
314
+
315
+ st, @error = validate_service_ticket(@service, @ticket)
316
+ @success = st && !@error
317
+
318
+ if @success
319
+ @username = st.username
320
+ if @pgt_url
321
+ pgt = generate_proxy_granting_ticket(@pgt_url, st)
322
+ @pgtiou = pgt.iou if pgt
323
+ end
324
+ @extra_attributes = st.ticket_granting_ticket.extra_attributes || {}
325
+ end
326
+
327
+ render :service_validate
328
+ end
329
+ end
330
+
331
+ # 2.6
332
+ class ProxyValidate < R '/proxyValidate'
333
+ include CASServer::CAS
334
+
335
+ # 2.6.1
336
+ def get
337
+ CASServer::Utils::log_controller_action(self.class, @input)
338
+
339
+ # required
340
+ @service = clean_service_url(@input['service'])
341
+ @ticket = @input['ticket']
342
+ # optional
343
+ @pgt_url = @input['pgtUrl']
344
+ @renew = @input['renew']
345
+
346
+ @proxies = []
347
+
348
+ t, @error = validate_proxy_ticket(@service, @ticket)
349
+ @success = t && !@error
350
+
351
+ @extra_attributes = {}
352
+ if @success
353
+ @username = t.username
354
+
355
+ if t.kind_of? CASServer::Models::ProxyTicket
356
+ @proxies << t.proxy_granting_ticket.service_ticket.service
357
+ end
358
+
359
+ if @pgt_url
360
+ pgt = generate_proxy_granting_ticket(@pgt_url, t)
361
+ @pgtiou = pgt.iou if pgt
362
+ end
363
+
364
+ @extra_attributes = t.ticket_granting_ticket.extra_attributes || {}
365
+ end
366
+
367
+ render :proxy_validate
368
+ end
369
+ end
370
+
371
+ class Proxy < R '/proxy'
372
+ include CASServer::CAS
373
+
374
+ # 2.7
375
+ def get
376
+ CASServer::Utils::log_controller_action(self.class, @input)
377
+
378
+ # required
379
+ @ticket = @input['pgt']
380
+ @target_service = @input['targetService']
381
+
382
+ pgt, @error = validate_proxy_granting_ticket(@ticket)
383
+ @success = pgt && !@error
384
+
385
+ if @success
386
+ @pt = generate_proxy_ticket(@target_service, pgt)
387
+ end
388
+
389
+ render :proxy
390
+ end
391
+ end
392
+
393
+ # Controller for obtaining login tickets.
394
+ # This is useful when you want to build a custom login form located on a
395
+ # remote server. Your form will have to include a valid login ticket
396
+ # value, and this can be fetched from the CAS server using this controller'
397
+ # POST method.
398
+ class LoginTicketDispenser < R '/loginTicket'
399
+ include CASServer::CAS
400
+
401
+ def get
402
+ CASServer::Utils::log_controller_action(self.class, @input)
403
+ $LOG.error("Tried to use login ticket dispenser with get method!")
404
+ @status = 500
405
+ "To generate a login ticket, you must make a POST request."
406
+ end
407
+
408
+ # Renders a page with a login ticket (and only the login ticket)
409
+ # in the response body.
410
+ def post
411
+ CASServer::Utils::log_controller_action(self.class, @input)
412
+ lt = generate_login_ticket
413
+
414
+ $LOG.debug("Dispensing login ticket #{lt} to host #{(env['HTTP_X_FORWARDED_FOR'] || env['REMOTE_HOST'] || env['REMOTE_ADDR']).inspect}")
415
+
416
+ @lt = lt.ticket
417
+
418
+ @lt
419
+ end
420
+ end
421
+
422
+ class Themes < R '/themes/(.+)'
423
+ MIME_TYPES = {'.css' => 'text/css', '.js' => 'text/javascript',
424
+ '.jpg' => 'image/jpeg'}
425
+ PATH = CASServer::Conf.themes_dir || File.expand_path(File.dirname(__FILE__))+'/../themes'
426
+
427
+ def get(path)@headers['Content-Type'] = MIME_TYPES[path[/\.\w+$/, 0]] || "text/plain"
428
+ unless path.include? ".." # prevent directory traversal attacks
429
+ @headers['X-Sendfile'] = "#{PATH}/#{path}"
430
+ else
431
+ @status = "403"
432
+ "403 - Invalid path"
433
+ end
434
+ end
435
+ end
436
+ end
@@ -0,0 +1,23 @@
1
+ $: << File.dirname(File.expand_path(__FILE__))
2
+
3
+ # Try to load local version of Picnic if possible (for development purposes)
4
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../../picnic/lib"
5
+ $: << File.dirname(File.expand_path(__FILE__))+"/../../vendor/picnic/lib"
6
+
7
+ begin
8
+ require 'picnic'
9
+ rescue LoadError => e
10
+ # make sure that the LoadError was about picnic and not something else
11
+ raise e unless e.to_s =~ /picnic/
12
+
13
+ require 'rubygems'
14
+
15
+ # make things backwards-compatible for rubygems < 0.9.0
16
+ unless Object.method_defined? :gem
17
+ alias gem require_gem
18
+ end
19
+
20
+ gem 'picnic'
21
+
22
+ require 'picnic'
23
+ end