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.
- data/Gemfile +21 -2
- data/Rakefile +6 -0
- data/bin/cap +16 -0
- data/bin/capify +16 -0
- data/bin/foreman +16 -0
- data/bin/lessc +16 -0
- data/bin/rackup +16 -0
- data/bin/rake2thor +16 -0
- data/bin/rubycas-server +12 -26
- data/bin/therubyracer +16 -0
- data/bin/thor +16 -0
- data/bin/tilt +16 -0
- data/bin/unicorn +16 -0
- data/bin/unicorn_rails +16 -0
- data/config.ru +2 -1
- data/config/deploy.rb +36 -0
- data/config/deploy/production.rb +4 -0
- data/config/deploy/staging.rb +4 -0
- data/config/recipes/base.rb +8 -0
- data/config/recipes/git.rb +10 -0
- data/config/recipes/nginx.rb +28 -0
- data/config/recipes/puma.rb +38 -0
- data/config/recipes/rubycas.rb +11 -0
- data/config/recipes/templates/nginx.erb +43 -0
- data/config/recipes/templates/puma.erb +13 -0
- data/config/recipes/templates/rubycas.erb +114 -0
- data/config/unicorn/development.rb +14 -0
- data/config/unicorn/production.rb +14 -0
- data/config/unicorn/staging.rb +14 -0
- data/lib/casserver.rb +2 -1
- data/lib/casserver/cas.rb +330 -4
- data/lib/casserver/server.rb +4 -8
- data/lib/casserver/views/_login_form.erb +15 -36
- data/lib/casserver/views/layout.erb +40 -4
- data/lib/casserver/views/login.erb +13 -27
- data/locales/en.yml +17 -3
- data/public/app.css +9641 -0
- data/public/assets/fontawesome-webfont.eot +0 -0
- data/public/assets/fontawesome-webfont.svg +255 -0
- data/public/assets/fontawesome-webfont.ttf +0 -0
- data/public/assets/fontawesome-webfont.woff +0 -0
- data/public/assets/gothamhtf-black-webfont.eot +0 -0
- data/public/assets/gothamhtf-black-webfont.svg +241 -0
- data/public/assets/gothamhtf-black-webfont.ttf +0 -0
- data/public/assets/gothamhtf-black-webfont.woff +0 -0
- data/public/assets/gothamhtf-blackitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-blackitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-blackitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-blackitalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-bold-webfont.eot +0 -0
- data/public/assets/gothamhtf-bold-webfont.svg +241 -0
- data/public/assets/gothamhtf-bold-webfont.ttf +0 -0
- data/public/assets/gothamhtf-bold-webfont.woff +0 -0
- data/public/assets/gothamhtf-bolditalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-bolditalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-bolditalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-bolditalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-book-webfont.eot +0 -0
- data/public/assets/gothamhtf-book-webfont.svg +241 -0
- data/public/assets/gothamhtf-book-webfont.ttf +0 -0
- data/public/assets/gothamhtf-book-webfont.woff +0 -0
- data/public/assets/gothamhtf-bookitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-bookitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-bookitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-bookitalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-light-webfont.eot +0 -0
- data/public/assets/gothamhtf-light-webfont.svg +241 -0
- data/public/assets/gothamhtf-light-webfont.ttf +0 -0
- data/public/assets/gothamhtf-light-webfont.woff +0 -0
- data/public/assets/gothamhtf-lightitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-lightitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-lightitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-lightitalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-medium-webfont.eot +0 -0
- data/public/assets/gothamhtf-medium-webfont.svg +241 -0
- data/public/assets/gothamhtf-medium-webfont.ttf +0 -0
- data/public/assets/gothamhtf-medium-webfont.woff +0 -0
- data/public/assets/gothamhtf-thin-webfont.eot +0 -0
- data/public/assets/gothamhtf-thin-webfont.svg +241 -0
- data/public/assets/gothamhtf-thin-webfont.ttf +0 -0
- data/public/assets/gothamhtf-thin-webfont.woff +0 -0
- data/public/assets/gothamhtf-thinitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-thinitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-thinitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-thinitalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-ultra-webfont.eot +0 -0
- data/public/assets/gothamhtf-ultra-webfont.svg +241 -0
- data/public/assets/gothamhtf-ultra-webfont.ttf +0 -0
- data/public/assets/gothamhtf-ultra-webfont.woff +0 -0
- data/public/assets/gothamhtf-ultraitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-ultraitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-ultraitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-ultraitalic-webfont.woff +0 -0
- data/public/assets/gothamhtf-xlight-webfont.eot +0 -0
- data/public/assets/gothamhtf-xlight-webfont.svg +241 -0
- data/public/assets/gothamhtf-xlight-webfont.ttf +0 -0
- data/public/assets/gothamhtf-xlight-webfont.woff +0 -0
- data/public/assets/gothamhtf-xlightitalic-webfont.eot +0 -0
- data/public/assets/gothamhtf-xlightitalic-webfont.svg +241 -0
- data/public/assets/gothamhtf-xlightitalic-webfont.ttf +0 -0
- data/public/assets/gothamhtf-xlightitalic-webfont.woff +0 -0
- data/public/css/app.css +190 -0
- data/public/css/bootstrap-responsive.min.css +9 -0
- data/public/css/bootstrap.min.css +9 -0
- data/public/img/glyphicons-halflings-white.png +0 -0
- data/public/img/glyphicons-halflings.png +0 -0
- data/public/js/app.js +0 -0
- data/public/js/bootstrap.min.js +6 -0
- data/public/js/jquery-1.8.0.js +9227 -0
- data/public/themes/app.css +4652 -0
- data/public/themes/cas.css +2 -0
- data/rubycas-server.gemspec +62 -0
- data/spec/casserver_spec.rb +1 -1
- data/spec/config/default_config.yml +1 -1
- metadata +146 -12
- checksums.yaml +0 -15
- data/config/unicorn.rb +0 -88
- 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
|