synapse-rubycas-server 1.1.4 → 1.1.5.pre

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 (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