synapse-rubycas-server 1.1.4 → 1.1.5.pre

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. data/Gemfile +21 -2
  2. data/Rakefile +6 -0
  3. data/bin/cap +16 -0
  4. data/bin/capify +16 -0
  5. data/bin/foreman +16 -0
  6. data/bin/lessc +16 -0
  7. data/bin/rackup +16 -0
  8. data/bin/rake2thor +16 -0
  9. data/bin/rubycas-server +12 -26
  10. data/bin/therubyracer +16 -0
  11. data/bin/thor +16 -0
  12. data/bin/tilt +16 -0
  13. data/bin/unicorn +16 -0
  14. data/bin/unicorn_rails +16 -0
  15. data/config.ru +2 -1
  16. data/config/deploy.rb +36 -0
  17. data/config/deploy/production.rb +4 -0
  18. data/config/deploy/staging.rb +4 -0
  19. data/config/recipes/base.rb +8 -0
  20. data/config/recipes/git.rb +10 -0
  21. data/config/recipes/nginx.rb +28 -0
  22. data/config/recipes/puma.rb +38 -0
  23. data/config/recipes/rubycas.rb +11 -0
  24. data/config/recipes/templates/nginx.erb +43 -0
  25. data/config/recipes/templates/puma.erb +13 -0
  26. data/config/recipes/templates/rubycas.erb +114 -0
  27. data/config/unicorn/development.rb +14 -0
  28. data/config/unicorn/production.rb +14 -0
  29. data/config/unicorn/staging.rb +14 -0
  30. data/lib/casserver.rb +2 -1
  31. data/lib/casserver/cas.rb +330 -4
  32. data/lib/casserver/server.rb +4 -8
  33. data/lib/casserver/views/_login_form.erb +15 -36
  34. data/lib/casserver/views/layout.erb +40 -4
  35. data/lib/casserver/views/login.erb +13 -27
  36. data/locales/en.yml +17 -3
  37. data/public/app.css +9641 -0
  38. data/public/assets/fontawesome-webfont.eot +0 -0
  39. data/public/assets/fontawesome-webfont.svg +255 -0
  40. data/public/assets/fontawesome-webfont.ttf +0 -0
  41. data/public/assets/fontawesome-webfont.woff +0 -0
  42. data/public/assets/gothamhtf-black-webfont.eot +0 -0
  43. data/public/assets/gothamhtf-black-webfont.svg +241 -0
  44. data/public/assets/gothamhtf-black-webfont.ttf +0 -0
  45. data/public/assets/gothamhtf-black-webfont.woff +0 -0
  46. data/public/assets/gothamhtf-blackitalic-webfont.eot +0 -0
  47. data/public/assets/gothamhtf-blackitalic-webfont.svg +241 -0
  48. data/public/assets/gothamhtf-blackitalic-webfont.ttf +0 -0
  49. data/public/assets/gothamhtf-blackitalic-webfont.woff +0 -0
  50. data/public/assets/gothamhtf-bold-webfont.eot +0 -0
  51. data/public/assets/gothamhtf-bold-webfont.svg +241 -0
  52. data/public/assets/gothamhtf-bold-webfont.ttf +0 -0
  53. data/public/assets/gothamhtf-bold-webfont.woff +0 -0
  54. data/public/assets/gothamhtf-bolditalic-webfont.eot +0 -0
  55. data/public/assets/gothamhtf-bolditalic-webfont.svg +241 -0
  56. data/public/assets/gothamhtf-bolditalic-webfont.ttf +0 -0
  57. data/public/assets/gothamhtf-bolditalic-webfont.woff +0 -0
  58. data/public/assets/gothamhtf-book-webfont.eot +0 -0
  59. data/public/assets/gothamhtf-book-webfont.svg +241 -0
  60. data/public/assets/gothamhtf-book-webfont.ttf +0 -0
  61. data/public/assets/gothamhtf-book-webfont.woff +0 -0
  62. data/public/assets/gothamhtf-bookitalic-webfont.eot +0 -0
  63. data/public/assets/gothamhtf-bookitalic-webfont.svg +241 -0
  64. data/public/assets/gothamhtf-bookitalic-webfont.ttf +0 -0
  65. data/public/assets/gothamhtf-bookitalic-webfont.woff +0 -0
  66. data/public/assets/gothamhtf-light-webfont.eot +0 -0
  67. data/public/assets/gothamhtf-light-webfont.svg +241 -0
  68. data/public/assets/gothamhtf-light-webfont.ttf +0 -0
  69. data/public/assets/gothamhtf-light-webfont.woff +0 -0
  70. data/public/assets/gothamhtf-lightitalic-webfont.eot +0 -0
  71. data/public/assets/gothamhtf-lightitalic-webfont.svg +241 -0
  72. data/public/assets/gothamhtf-lightitalic-webfont.ttf +0 -0
  73. data/public/assets/gothamhtf-lightitalic-webfont.woff +0 -0
  74. data/public/assets/gothamhtf-medium-webfont.eot +0 -0
  75. data/public/assets/gothamhtf-medium-webfont.svg +241 -0
  76. data/public/assets/gothamhtf-medium-webfont.ttf +0 -0
  77. data/public/assets/gothamhtf-medium-webfont.woff +0 -0
  78. data/public/assets/gothamhtf-thin-webfont.eot +0 -0
  79. data/public/assets/gothamhtf-thin-webfont.svg +241 -0
  80. data/public/assets/gothamhtf-thin-webfont.ttf +0 -0
  81. data/public/assets/gothamhtf-thin-webfont.woff +0 -0
  82. data/public/assets/gothamhtf-thinitalic-webfont.eot +0 -0
  83. data/public/assets/gothamhtf-thinitalic-webfont.svg +241 -0
  84. data/public/assets/gothamhtf-thinitalic-webfont.ttf +0 -0
  85. data/public/assets/gothamhtf-thinitalic-webfont.woff +0 -0
  86. data/public/assets/gothamhtf-ultra-webfont.eot +0 -0
  87. data/public/assets/gothamhtf-ultra-webfont.svg +241 -0
  88. data/public/assets/gothamhtf-ultra-webfont.ttf +0 -0
  89. data/public/assets/gothamhtf-ultra-webfont.woff +0 -0
  90. data/public/assets/gothamhtf-ultraitalic-webfont.eot +0 -0
  91. data/public/assets/gothamhtf-ultraitalic-webfont.svg +241 -0
  92. data/public/assets/gothamhtf-ultraitalic-webfont.ttf +0 -0
  93. data/public/assets/gothamhtf-ultraitalic-webfont.woff +0 -0
  94. data/public/assets/gothamhtf-xlight-webfont.eot +0 -0
  95. data/public/assets/gothamhtf-xlight-webfont.svg +241 -0
  96. data/public/assets/gothamhtf-xlight-webfont.ttf +0 -0
  97. data/public/assets/gothamhtf-xlight-webfont.woff +0 -0
  98. data/public/assets/gothamhtf-xlightitalic-webfont.eot +0 -0
  99. data/public/assets/gothamhtf-xlightitalic-webfont.svg +241 -0
  100. data/public/assets/gothamhtf-xlightitalic-webfont.ttf +0 -0
  101. data/public/assets/gothamhtf-xlightitalic-webfont.woff +0 -0
  102. data/public/css/app.css +190 -0
  103. data/public/css/bootstrap-responsive.min.css +9 -0
  104. data/public/css/bootstrap.min.css +9 -0
  105. data/public/img/glyphicons-halflings-white.png +0 -0
  106. data/public/img/glyphicons-halflings.png +0 -0
  107. data/public/js/app.js +0 -0
  108. data/public/js/bootstrap.min.js +6 -0
  109. data/public/js/jquery-1.8.0.js +9227 -0
  110. data/public/themes/app.css +4652 -0
  111. data/public/themes/cas.css +2 -0
  112. data/rubycas-server.gemspec +62 -0
  113. data/spec/casserver_spec.rb +1 -1
  114. data/spec/config/default_config.yml +1 -1
  115. metadata +146 -12
  116. checksums.yaml +0 -15
  117. data/config/unicorn.rb +0 -88
  118. data/public/themes/simple/theme.css +0 -28
@@ -0,0 +1,43 @@
1
+ upstream <%= application %> {
2
+ server unix:<%= shared_path %>/sockets/<%= application %>-puma.sock fail_timeout=0;
3
+ }
4
+
5
+ server {
6
+ listen 80;
7
+ server_name <%= server_name %>;
8
+ rewrite ^ https://$server_name$request_uri? permanent;
9
+ }
10
+
11
+ server {
12
+ listen 443;
13
+ server_name <%= server_name %>;
14
+ root <%= current_path %>/public;
15
+
16
+ ssl on;
17
+ ssl_certificate /etc/ssl/cert_wildcard.crt;
18
+ ssl_certificate_key /etc/ssl/key_wildcard.key;
19
+
20
+ if (-f $document_root/maintenance.html) {
21
+ rewrite ^(.*)$ /maintenance.html last;
22
+ break;
23
+ }
24
+
25
+ location / {
26
+ try_files $uri $uri/index.html $uri @<%= application %>;
27
+ }
28
+
29
+
30
+ location @<%= application %> {
31
+ proxy_redirect off;
32
+ proxy_set_header X-FORWARDED_PROTO https;
33
+ proxy_set_header Host $http_host;
34
+ proxy_set_header X-Real-IP $remote_addr;
35
+ proxy_read_timeout 300;
36
+ proxy_connect_timeout 300;
37
+ proxy_pass http://<%= application %>;
38
+ }
39
+
40
+ error_page 500 502 503 504 /500.html;
41
+ client_max_body_size 4G;
42
+ keepalive_timeout 10;
43
+ }
@@ -0,0 +1,13 @@
1
+ environment '<%= stage %>' || 'development'
2
+
3
+ threads 3,3
4
+
5
+ bind "unix:///<%= puma_sock %>"
6
+ pidfile "<%= puma_pid %>"
7
+ state_path "<%= puma_state %>"
8
+
9
+ daemonize true
10
+
11
+ stdout_redirect '<%= shared_path %>/log/puma.stdout.log', '<%= shared_path %>/log/puma.stderror.log', true
12
+
13
+ activate_control_app "unix:///<%= puma_ctl_sock %>"
@@ -0,0 +1,114 @@
1
+ # MANAGED BY CAPISTRANO
2
+
3
+
4
+ ##### DATABASE #################################################################
5
+
6
+ database:
7
+ adapter: mysql2
8
+ database: casserver_dev
9
+ username: casserver_user
10
+ password: DaDLw7a7oXZVWz
11
+ host: core-internal.cg3ywao0k1nv.us-west-2.rds.amazonaws.com
12
+ reconnect: true
13
+
14
+ #disable_auto_migrations: true
15
+
16
+ ##### AUTHENTICATION ###########################################################
17
+
18
+ authenticator:
19
+ class: CASServer::Authenticators::ActiveDirectoryLDAP
20
+ ldap:
21
+ host: core-dc-1.synapsedev.com
22
+ port: 389
23
+ base: OU=SBSUsers,OU=Users,OU=MyBusiness,DC=synapsedev,DC=com
24
+ filter: (&(objectCategory=person)(objectClass=user))
25
+ auth_user: SYNAPSEDEV\~svcProvisioningConf
26
+ auth_password: IZE6CgJhIZZYbKfyW4Po
27
+ extra_attributes: name, mail, memberOf, synapseRecursiveGroups, synapseExtendedAttributes, givenname, sn, pinNumber, department, company, l, synapseAccessCardNumber
28
+
29
+ theme: simple
30
+
31
+ organization: Synapse
32
+
33
+ infoline: Powered by <a href="http://code.google.com/p/rubycas-server/">RubyCAS-Server</a>
34
+
35
+ # Custom views directory. If set, this will be used instead of 'lib/casserver/views'.
36
+ custom_views: <%= deploy_to %>/current/lib/casserver/views
37
+
38
+ # Custom public directory. If set, static content (css, etc.) will be served from here rather
39
+ # than from rubycas-server's internal 'public' directory (but be mindful of any overriding
40
+ # settings you may have in your web server's config).
41
+ public_dir: <%= deploy_to %>/current/public
42
+
43
+ default_locale: en
44
+
45
+ ##### LOGGING ##################################################################
46
+
47
+ log:
48
+ file: <%= shared_path %>/log/casserver.log
49
+ level: DEBUG
50
+
51
+
52
+ # If you want full database logging, uncomment this next section.
53
+ # Every SQL query will be logged here. This is useful for debugging database
54
+ # problems.
55
+
56
+ #db_log:
57
+ # file: /var/log/casserver_db.log
58
+
59
+
60
+ # Setting the following option to true will disable CLI output to stdout.
61
+ # i.e. this will get rid of messages like ">>> Redirecting RubyCAS-Server log..."
62
+ # This is useful when, for example, you're running rspecs.
63
+
64
+ #quiet: true
65
+
66
+
67
+ ##### SINGLE SIGN-OUT ##########################################################
68
+
69
+ # When a user logs in to a CAS-enabled client application, that application
70
+ # generally opens its own local user session. When the user then logs out
71
+ # through the CAS server, each of the CAS-enabled client applications need
72
+ # to be notified so that they can close their own local sessions for that user.
73
+ #
74
+ # Up until recently this was not possible within CAS. However, a method for
75
+ # performing this notification was recently added to the protocol (in CAS 3.1).
76
+ # This works exactly as described above -- when the user logs out, the CAS
77
+ # server individually contacts each client service and notifies it of the
78
+ # logout. Currently not all client applications support this, so this
79
+ # behaviour is disabled by default. To enable it, uncomment the following
80
+ # configuration line. Note that currently it is not possible to enable
81
+ # or disable single-sign-out on a per-service basis, but this functionality
82
+ # is planned for a future release.
83
+
84
+ enable_single_sign_out: true
85
+
86
+
87
+ ##### OTHER ####################################################################
88
+
89
+ # You can set various ticket expiry times (specify the value in seconds).
90
+
91
+ # Unused login and service tickets become unusable this many seconds after
92
+ # they are created. (Defaults to 5 minutes)
93
+
94
+ #maximum_unused_login_ticket_lifetime: 300
95
+ #maximum_unused_service_ticket_lifetime: 300
96
+
97
+ # The server must periodically delete old tickets (login tickets, service tickets
98
+ # proxy-granting tickets, and ticket-granting tickets) to prevent buildup of
99
+ # stale data. This effectively limits the maximum length of a CAS session to
100
+ # the lifetime given here (in seconds). (Defaults to 48 hours)
101
+ #
102
+ # Note that this limit is not enforced on the client side; it refers only to the
103
+ # the maximum lifetime of tickets on the CAS server.
104
+
105
+ #maximum_session_lifetime: 172800
106
+
107
+
108
+ # If you want the usernames entered on the login page to be automatically
109
+ # downcased (converted to lowercase), enable the following option. When this
110
+ # option is set to true, if the user enters "JSmith" as their username, the
111
+ # system will automatically
112
+ # convert this to "jsmith".
113
+
114
+ downcase_username: true
@@ -0,0 +1,14 @@
1
+ # MANAGED BY PUPPET
2
+ # Module:: rubycas
3
+ #
4
+
5
+ worker_processes 2
6
+ user 'vagrant', 'vagrant'
7
+ working_directory "/home/vagrant/rubycas-server/current"
8
+
9
+ pid "/home/vagrant/rubycas-server/shared/pids/unicorn.pid"
10
+
11
+ listen 8001
12
+
13
+ stderr_path "/home/vagrant/rubycas-server/current/log/unicorn.stderr.log"
14
+ stdout_path "/home/vagrant/rubycas-server/current/log/unicorn.stdout.log"
@@ -0,0 +1,14 @@
1
+ # MANAGED BY PUPPET
2
+ # Module:: rubycas
3
+ #
4
+
5
+ worker_processes 2
6
+ user 'rubycas-server', 'rubycas-server'
7
+ working_directory "/home/rubycas-server/rubycas-server/current"
8
+
9
+ pid "/home/rubycas-server/rubycas-server/shared/pids/unicorn.pid"
10
+
11
+ listen 8001
12
+
13
+ stderr_path "/home/rubycas-server/rubycas-server/current/log/unicorn.stderr.log"
14
+ stdout_path "/home/rubycas-server/rubycas-server/current/log/unicorn.stdout.log"
@@ -0,0 +1,14 @@
1
+ # MANAGED BY PUPPET
2
+ # Module:: rubycas
3
+ #
4
+
5
+ worker_processes 2
6
+ user 'rubycas', 'rubycas'
7
+ working_directory "/home/rubycas/rubycas-server/current"
8
+ preload_app true
9
+ pid "/home/rubycas/rubycas-server/shared/pids/unicorn.pid"
10
+
11
+ listen 8001
12
+
13
+ stderr_path "/home/rubycas/rubycas-server/current/log/unicorn.stderr.log"
14
+ stdout_path "/home/rubycas/rubycas-server/current/log/unicorn.stdout.log"
data/lib/casserver.rb CHANGED
@@ -3,9 +3,10 @@ module CASServer; end
3
3
  require 'active_record'
4
4
  require 'active_support'
5
5
  require 'sinatra/base'
6
- require 'casserver/core_ext/directory_user'
7
6
  require 'builder' # for XML views
8
7
  require 'logger'
8
+ require 'net/ldap'
9
+ require 'casserver/core_ext/directory_user'
9
10
  $LOG = Logger.new(STDOUT)
10
11
 
11
12
  require 'casserver/authenticators/base'
data/lib/casserver/cas.rb CHANGED
@@ -34,6 +34,332 @@ module CASServer::CAS
34
34
  tgt.ticket = "TGC-" + String.random
35
35
  tgt.username = username
36
36
  tgt.extra_attributes = extra_attributes
37
+ tgt.expires = (Time.now + 2.weeks).strftime("%a, %d-%b-%Y %H:%M:%S GMT")
38
+ tgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
39
+ tgt.save!
40
+ $LOG.debug("Generated ticket granting ticket '#{tgt.ticket}' for user" +
41
+ " '#{tgt.username}' at '#{tgt.client_hostname}' with expiration of '#{tgt.expires}'" +
42
+ (extra_attributes.blank? ? "" : " with extra attributes #{extra_attributes.inspect}"))
43
+ tgt
44
+ end
45
+
46
+ def generate_service_ticket(service, username, tgt)
47
+ # 3.1 (service ticket)
48
+ st = ServiceTicket.new
49
+ st.ticket = "ST-" + String.random
50
+ st.service = service
51
+ st.username = username
52
+ st.granted_by_tgt_id = tgt.id
53
+ st.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
54
+ st.save!
55
+ $LOG.debug("Generated service ticket '#{st.ticket}' for service '#{st.service}'" +
56
+ " for user '#{st.username}' at '#{st.client_hostname}'")
57
+ st
58
+ end
59
+
60
+ def generate_proxy_ticket(target_service, pgt)
61
+ # 3.2 (proxy ticket)
62
+ pt = ProxyTicket.new
63
+ pt.ticket = "PT-" + String.random
64
+ pt.service = target_service
65
+ pt.username = pgt.service_ticket.username
66
+ pt.granted_by_pgt_id = pgt.id
67
+ pt.granted_by_tgt_id = pgt.service_ticket.granted_by_tgt_id
68
+ pt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
69
+ pt.save!
70
+ $LOG.debug("Generated proxy ticket '#{pt.ticket}' for target service '#{pt.service}'" +
71
+ " for user '#{pt.username}' at '#{pt.client_hostname}' using proxy-granting" +
72
+ " ticket '#{pgt.ticket}'")
73
+ pt
74
+ end
75
+
76
+ def generate_proxy_granting_ticket(pgt_url, st)
77
+ uri = URI.parse(pgt_url)
78
+ https = Net::HTTP.new(uri.host,uri.port)
79
+ https.use_ssl = true
80
+
81
+ # Here's what's going on here:
82
+ #
83
+ # 1. We generate a ProxyGrantingTicket (but don't store it in the database just yet)
84
+ # 2. Deposit the PGT and it's associated IOU at the proxy callback URL.
85
+ # 3. If the proxy callback URL responds with HTTP code 200, store the PGT and return it;
86
+ # otherwise don't save it and return nothing.
87
+ #
88
+ https.start do |conn|
89
+ path = uri.path.empty? ? '/' : uri.path
90
+ path += '?' + uri.query unless (uri.query.nil? || uri.query.empty?)
91
+
92
+ pgt = ProxyGrantingTicket.new
93
+ pgt.ticket = "PGT-" + String.random(60)
94
+ pgt.iou = "PGTIOU-" + String.random(57)
95
+ pgt.service_ticket_id = st.id
96
+ pgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
97
+
98
+ # FIXME: The CAS protocol spec says to use 'pgt' as the parameter, but in practice
99
+ # the JA-SIG and Yale server implementations use pgtId. We'll go with the
100
+ # in-practice standard.
101
+ path += (uri.query.nil? || uri.query.empty? ? '?' : '&') + "pgtId=#{pgt.ticket}&pgtIou=#{pgt.iou}"
102
+
103
+ response = conn.request_get(path)
104
+ # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
105
+ # NOTE: The following response codes are valid according to the JA-SIG implementation even without following redirects
106
+
107
+ if %w(200 202 301 302 304).include?(response.code)
108
+ # 3.4 (proxy-granting ticket IOU)
109
+ pgt.save!
110
+ $LOG.debug "PGT generated for pgt_url '#{pgt_url}': #{pgt.inspect}"
111
+ pgt
112
+ else
113
+ $LOG.warn "PGT callback server responded with a bad result code '#{response.code}'. PGT will not be stored."
114
+ nil
115
+ end
116
+ end
117
+ end
118
+
119
+ def validate_login_ticket(ticket)
120
+ $LOG.debug("Validating login ticket '#{ticket}'")
121
+
122
+ success = false
123
+ if ticket.nil?
124
+ error = t.error.no_login_ticket
125
+ $LOG.warn "Missing login ticket."
126
+ elsif lt = LoginTicket.find_by_ticket(ticket)
127
+ if lt.consumed?
128
+ error = t.error.login_ticket_already_used
129
+ $LOG.warn "Login ticket '#{ticket}' previously used up"
130
+ elsif Time.now - lt.created_on < settings.config[:maximum_unused_login_ticket_lifetime]
131
+ $LOG.info "Login ticket '#{ticket}' successfully validated"
132
+ else
133
+ error = t.error.login_timeout
134
+ $LOG.warn "Expired login ticket '#{ticket}'"
135
+ end
136
+ else
137
+ error = t.error.invalid_login_ticket
138
+ $LOG.warn "Invalid login ticket '#{ticket}'"
139
+ end
140
+
141
+ lt.consume! if lt
142
+
143
+ error
144
+ end
145
+
146
+ def validate_ticket_granting_ticket(ticket)
147
+ $LOG.debug("Validating ticket granting ticket '#{ticket}'")
148
+
149
+ if ticket.nil?
150
+ error = "No ticket granting ticket given."
151
+ $LOG.debug error
152
+ elsif tgt = TicketGrantingTicket.find_by_ticket(ticket)
153
+ if settings.config[:maximum_session_lifetime] && Time.now - tgt.created_on > settings.config[:maximum_session_lifetime]
154
+ tgt.destroy
155
+ error = "Your session has expired. Please log in again."
156
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' expired."
157
+ else
158
+ $LOG.info "Ticket granting ticket '#{ticket}' for user '#{tgt.username}' successfully validated."
159
+ end
160
+ else
161
+ error = "Invalid ticket granting ticket '#{ticket}' (no matching ticket found in the database)."
162
+ $LOG.warn(error)
163
+ end
164
+
165
+ [tgt, error]
166
+ end
167
+
168
+ def validate_service_ticket(service, ticket, allow_proxy_tickets = false)
169
+ $LOG.debug "Validating service/proxy ticket '#{ticket}' for service '#{service}'"
170
+
171
+ if service.nil? or ticket.nil?
172
+ error = Error.new(:INVALID_REQUEST, "Ticket or service parameter was missing in the request.")
173
+ $LOG.warn "#{error.code} - #{error.message}"
174
+ elsif st = ServiceTicket.find_by_ticket(ticket)
175
+ if st.consumed?
176
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has already been used up.")
177
+ $LOG.warn "#{error.code} - #{error.message}"
178
+ elsif st.kind_of?(CASServer::Model::ProxyTicket) && !allow_proxy_tickets
179
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' is a proxy ticket, but only service tickets are allowed here.")
180
+ $LOG.warn "#{error.code} - #{error.message}"
181
+ elsif Time.now - st.created_on > settings.config[:maximum_unused_service_ticket_lifetime]
182
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' has expired.")
183
+ $LOG.warn "Ticket '#{ticket}' has expired."
184
+ elsif !st.matches_service? service
185
+ error = Error.new(:INVALID_SERVICE, "The ticket '#{ticket}' belonging to user '#{st.username}' is valid,"+
186
+ " but the requested service '#{service}' does not match the service '#{st.service}' associated with this ticket.")
187
+ $LOG.warn "#{error.code} - #{error.message}"
188
+ else
189
+ $LOG.info("Ticket '#{ticket}' for service '#{service}' for user '#{st.username}' successfully validated.")
190
+ end
191
+ else
192
+ error = Error.new(:INVALID_TICKET, "Ticket '#{ticket}' not recognized.")
193
+ $LOG.warn("#{error.code} - #{error.message}")
194
+ end
195
+
196
+ if st
197
+ st.consume!
198
+ end
199
+
200
+
201
+ [st, error]
202
+ end
203
+
204
+ def validate_proxy_ticket(service, ticket)
205
+ pt, error = validate_service_ticket(service, ticket, true)
206
+
207
+ if pt.kind_of?(CASServer::Model::ProxyTicket) && !error
208
+ if not pt.granted_by_pgt
209
+ error = Error.new(:INTERNAL_ERROR, "Proxy ticket '#{pt}' belonging to user '#{pt.username}' is not associated with a proxy granting ticket.")
210
+ elsif not pt.granted_by_pgt.service_ticket
211
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{pt.granted_by_pgt}'"+
212
+ " (associated with proxy ticket '#{pt}' and belonging to user '#{pt.username}' is not associated with a service ticket.")
213
+ end
214
+ end
215
+
216
+ [pt, error]
217
+ end
218
+
219
+ def validate_proxy_granting_ticket(ticket)
220
+ if ticket.nil?
221
+ error = Error.new(:INVALID_REQUEST, "pgt parameter was missing in the request.")
222
+ $LOG.warn("#{error.code} - #{error.message}")
223
+ elsif pgt = ProxyGrantingTicket.find_by_ticket(ticket)
224
+ if pgt.service_ticket
225
+ $LOG.info("Proxy granting ticket '#{ticket}' belonging to user '#{pgt.service_ticket.username}' successfully validated.")
226
+ else
227
+ error = Error.new(:INTERNAL_ERROR, "Proxy granting ticket '#{ticket}' is not associated with a service ticket.")
228
+ $LOG.error("#{error.code} - #{error.message}")
229
+ end
230
+ else
231
+ error = Error.new(:BAD_PGT, "Invalid proxy granting ticket '#{ticket}' (no matching ticket found in the database).")
232
+ $LOG.warn("#{error.code} - #{error.message}")
233
+ end
234
+
235
+ [pgt, error]
236
+ end
237
+
238
+ # Takes an existing ServiceTicket object (presumably pulled from the database)
239
+ # and sends a POST with logout information to the service that the ticket
240
+ # was generated for.
241
+ #
242
+ # This makes possible the "single sign-out" functionality added in CAS 3.1.
243
+ # See http://www.ja-sig.org/wiki/display/CASUM/Single+Sign+Out
244
+ def send_logout_notification_for_service_ticket(st)
245
+ uri = URI.parse(st.service)
246
+ uri.path = '/' if uri.path.empty?
247
+ time = Time.now
248
+ rand = String.random
249
+ path = uri.path
250
+ req = Net::HTTP::Post.new(path)
251
+ req.set_form_data('logoutRequest' => %{<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="#{rand}" Version="2.0" IssueInstant="#{time.rfc2822}">
252
+ <saml:NameID></saml:NameID>
253
+ <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
254
+ </samlp:LogoutRequest>})
255
+
256
+ begin
257
+ http = Net::HTTP.new(uri.host, uri.port)
258
+ http.use_ssl = true if uri.scheme =='https'
259
+
260
+ http.start do |conn|
261
+ response = conn.request(req)
262
+ if response.kind_of? Net::HTTPSuccess
263
+ $LOG.info "Logout notification successfully posted to #{st.service.inspect}."
264
+ return true
265
+ else
266
+ $LOG.error "Service #{st.service.inspect} responed to logout notification with code '#{response.code}'!"
267
+ return false
268
+ end
269
+ end
270
+ rescue Exception => e
271
+ $LOG.error "Failed to send logout notification to service #{st.service.inspect} due to #{e}"
272
+ return false
273
+ end
274
+ end
275
+
276
+ def service_uri_with_ticket(service, st)
277
+ raise ArgumentError, "Second argument must be a ServiceTicket!" unless st.kind_of? CASServer::Model::ServiceTicket
278
+
279
+ # This will choke with a URI::InvalidURIError if service URI is not properly URI-escaped...
280
+ # This exception is handled further upstream (i.e. in the controller).
281
+ service_uri = URI.parse(service)
282
+
283
+ if service.include? "?"
284
+ if service_uri.query.empty?
285
+ query_separator = ""
286
+ else
287
+ query_separator = "&"
288
+ end
289
+ else
290
+ query_separator = "?"
291
+ end
292
+
293
+ service_with_ticket = service + query_separator + "ticket=" + st.ticket
294
+ service_with_ticket
295
+ end
296
+
297
+ # Strips CAS-related parameters from a service URL and normalizes it,
298
+ # removing trailing / and ?. Also converts any spaces to +.
299
+ #
300
+ # For example, "http://google.com?ticket=12345" will be returned as
301
+ # "http://google.com". Also, "http://google.com/" would be returned as
302
+ # "http://google.com".
303
+ #
304
+ # Note that only the first occurance of each CAS-related parameter is
305
+ # removed, so that "http://google.com?ticket=12345&ticket=abcd" would be
306
+ # returned as "http://google.com?ticket=abcd".
307
+ def clean_service_url(dirty_service)
308
+ return dirty_service if dirty_service.blank?
309
+ clean_service = dirty_service.dup
310
+ ['service', 'ticket', 'gateway', 'renew'].each do |p|
311
+ clean_service.sub!(Regexp.new("&?#{p}=[^&]*"), '')
312
+ end
313
+
314
+ clean_service.gsub!(/[\/\?&]$/, '') # remove trailing ?, /, or &
315
+ clean_service.gsub!('?&', '?')
316
+ clean_service.gsub!(' ', '+')
317
+
318
+ $LOG.debug("Cleaned dirty service URL #{dirty_service.inspect} to #{clean_service.inspect}") if
319
+ dirty_service != clean_service
320
+
321
+ return clean_service
322
+ end
323
+ module_function :clean_service_url
324
+
325
+ end
326
+ require 'uri'
327
+ require 'net/https'
328
+
329
+ require 'casserver/model'
330
+ require 'casserver/core_ext'
331
+
332
+ # Encapsulates CAS functionality. This module is meant to be included in
333
+ # the CASServer::Controllers module.
334
+ module CASServer::CAS
335
+
336
+ include CASServer::Model
337
+
338
+ def generate_login_ticket
339
+ # 3.5 (login ticket)
340
+ lt = LoginTicket.new
341
+ lt.ticket = "LT-" + String.random
342
+
343
+ lt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
344
+ lt.save!
345
+ $LOG.debug("Generated login ticket '#{lt.ticket}' for client" +
346
+ " at '#{lt.client_hostname}'")
347
+ lt
348
+ end
349
+
350
+ # Creates a TicketGrantingTicket for the given username. This is done when the user logs in
351
+ # for the first time to establish their SSO session (after their credentials have been validated).
352
+ #
353
+ # The optional 'extra_attributes' parameter takes a hash of additional attributes
354
+ # that will be sent along with the username in the CAS response to subsequent
355
+ # validation requests from clients.
356
+ def generate_ticket_granting_ticket(username, extra_attributes = {})
357
+ # 3.6 (ticket granting cookie/ticket)
358
+ tgt = TicketGrantingTicket.new
359
+ tgt.ticket = "TGC-" + String.random
360
+ tgt.username = username
361
+ tgt.extra_attributes = extra_attributes
362
+ # tgt.expires = (Time.now + 2.weeks).strftime("%a, %d-%b-%Y %H:%M:%S GMT")
37
363
  tgt.client_hostname = @env['HTTP_X_FORWARDED_FOR'] || @env['REMOTE_HOST'] || @env['REMOTE_ADDR']
38
364
  tgt.save!
39
365
  $LOG.debug("Generated ticket granting ticket '#{tgt.ticket}' for user" +
@@ -87,7 +413,7 @@ module CASServer::CAS
87
413
  https.start do |conn|
88
414
  path = uri.path.empty? ? '/' : uri.path
89
415
  path += '?' + uri.query unless (uri.query.nil? || uri.query.empty?)
90
-
416
+
91
417
  pgt = ProxyGrantingTicket.new
92
418
  pgt.ticket = "PGT-" + String.random(60)
93
419
  pgt.iou = "PGTIOU-" + String.random(57)
@@ -102,7 +428,7 @@ module CASServer::CAS
102
428
  response = conn.request_get(path)
103
429
  # TODO: follow redirects... 2.5.4 says that redirects MAY be followed
104
430
  # NOTE: The following response codes are valid according to the JA-SIG implementation even without following redirects
105
-
431
+
106
432
  if %w(200 202 301 302 304).include?(response.code)
107
433
  # 3.4 (proxy-granting ticket IOU)
108
434
  pgt.save!
@@ -251,11 +577,11 @@ module CASServer::CAS
251
577
  <saml:NameID></saml:NameID>
252
578
  <samlp:SessionIndex>#{st.ticket}</samlp:SessionIndex>
253
579
  </samlp:LogoutRequest>})
254
-
580
+
255
581
  begin
256
582
  http = Net::HTTP.new(uri.host, uri.port)
257
583
  http.use_ssl = true if uri.scheme =='https'
258
-
584
+
259
585
  http.start do |conn|
260
586
  response = conn.request(req)
261
587
  if response.kind_of? Net::HTTPSuccess