jorahood-rubycas-client 2.0.99.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/CHANGELOG.txt +1 -0
- data/History.txt +162 -0
- data/LICENSE.txt +504 -0
- data/Manifest.txt +23 -0
- data/README.rdoc +334 -0
- data/Rakefile +63 -0
- data/init.rb +6 -0
- data/lib/casclient.rb +89 -0
- data/lib/casclient/client.rb +211 -0
- data/lib/casclient/frameworks/merb/strategy.rb +110 -0
- data/lib/casclient/frameworks/rails/cas_proxy_callback_controller.rb +76 -0
- data/lib/casclient/frameworks/rails/filter.rb +195 -0
- data/lib/casclient/responses.rb +179 -0
- data/lib/casclient/tickets.rb +38 -0
- data/lib/casclient/version.rb +9 -0
- data/lib/rubycas-client.rb +5 -0
- data/setup.rb +1585 -0
- metadata +94 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
module CASClient
|
|
2
|
+
# The client brokers all HTTP transactions with the CAS server.
|
|
3
|
+
class Client
|
|
4
|
+
attr_reader :cas_base_url
|
|
5
|
+
attr_reader :log, :username_session_key, :extra_attributes_session_key
|
|
6
|
+
attr_writer :login_url, :validate_url, :proxy_url, :logout_url, :service_url
|
|
7
|
+
attr_accessor :proxy_callback_url, :proxy_retrieval_url
|
|
8
|
+
|
|
9
|
+
def initialize(conf = nil)
|
|
10
|
+
configure(conf) if conf
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configure(conf)
|
|
14
|
+
raise ArgumentError, "Missing :cas_base_url parameter!" unless conf[:cas_base_url]
|
|
15
|
+
|
|
16
|
+
@cas_base_url = conf[:cas_base_url].gsub(/\/$/, '')
|
|
17
|
+
|
|
18
|
+
@login_url = conf[:login_url]
|
|
19
|
+
@logout_url = conf[:logout_url]
|
|
20
|
+
@validate_url = conf[:validate_url]
|
|
21
|
+
@proxy_url = conf[:proxy_url]
|
|
22
|
+
@service_url = conf[:service_url]
|
|
23
|
+
@proxy_callback_url = conf[:proxy_callback_url]
|
|
24
|
+
@proxy_retrieval_url = conf[:proxy_retrieval_url]
|
|
25
|
+
|
|
26
|
+
@username_session_key = conf[:username_session_key] || :cas_user
|
|
27
|
+
@extra_attributes_session_key = conf[:extra_attributes_session_key] || :cas_extra_attributes
|
|
28
|
+
|
|
29
|
+
@log = CASClient::LoggerWrapper.new
|
|
30
|
+
@log.set_real_logger(conf[:logger]) if conf[:logger]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def login_url
|
|
34
|
+
@login_url || (cas_base_url + "/login")
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def validate_url
|
|
38
|
+
@validate_url || (cas_base_url + "/proxyValidate")
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Returns the CAS server's logout url.
|
|
42
|
+
#
|
|
43
|
+
# If a logout_url has not been explicitly configured,
|
|
44
|
+
# the default is cas_base_url + "/logout".
|
|
45
|
+
#
|
|
46
|
+
# service_url:: Set this if you want the user to be
|
|
47
|
+
# able to immediately log back in. Generally
|
|
48
|
+
# you'll want to use something like <tt>request.referer</tt>.
|
|
49
|
+
# Note that this only works with RubyCAS-Server.
|
|
50
|
+
# back_url:: This satisfies section 2.3.1 of the CAS protocol spec.
|
|
51
|
+
# See http://www.ja-sig.org/products/cas/overview/protocol
|
|
52
|
+
def logout_url(service_url = nil, back_url = nil)
|
|
53
|
+
url = @logout_url || (cas_base_url + "/logout")
|
|
54
|
+
|
|
55
|
+
if service_url || back_url
|
|
56
|
+
uri = URI.parse(url)
|
|
57
|
+
h = uri.query ? query_to_hash(uri.query) : {}
|
|
58
|
+
h['service'] = service_url if service_url
|
|
59
|
+
h['url'] = back_url if back_url
|
|
60
|
+
uri.query = hash_to_query(h)
|
|
61
|
+
uri.to_s
|
|
62
|
+
else
|
|
63
|
+
url
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def proxy_url
|
|
68
|
+
@proxy_url || (cas_base_url + "/proxy")
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def validate_service_ticket(st)
|
|
72
|
+
uri = URI.parse(validate_url)
|
|
73
|
+
h = uri.query ? query_to_hash(uri.query) : {}
|
|
74
|
+
h['casurl'] = st.service
|
|
75
|
+
h['casticket'] = st.ticket
|
|
76
|
+
h['renew'] = 1 if st.renew
|
|
77
|
+
h['pgtUrl'] = proxy_callback_url if proxy_callback_url
|
|
78
|
+
uri.query = hash_to_query(h)
|
|
79
|
+
|
|
80
|
+
st.response = request_cas_response(uri, ValidationResponse)
|
|
81
|
+
|
|
82
|
+
return st
|
|
83
|
+
end
|
|
84
|
+
alias validate_proxy_ticket validate_service_ticket
|
|
85
|
+
|
|
86
|
+
# Requests a login using the given credentials for the given service;
|
|
87
|
+
# returns a LoginResponse object.
|
|
88
|
+
def login_to_service(credentials, service)
|
|
89
|
+
lt = request_login_ticket
|
|
90
|
+
|
|
91
|
+
data = credentials.merge(
|
|
92
|
+
:lt => lt,
|
|
93
|
+
:service => service
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
res = submit_data_to_cas(login_url, data)
|
|
97
|
+
CASClient::LoginResponse.new(res)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Requests a login ticket from the CAS server for use in a login request;
|
|
101
|
+
# returns a LoginTicket object.
|
|
102
|
+
#
|
|
103
|
+
# This only works with RubyCAS-Server, since obtaining login
|
|
104
|
+
# tickets in this manner is not part of the official CAS spec.
|
|
105
|
+
def request_login_ticket
|
|
106
|
+
uri = URI.parse(login_url+'Ticket')
|
|
107
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
|
108
|
+
https.use_ssl = (uri.scheme == 'https')
|
|
109
|
+
res = https.post(uri.path, ';')
|
|
110
|
+
|
|
111
|
+
raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
|
|
112
|
+
|
|
113
|
+
res.body.strip
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Requests a proxy ticket from the CAS server for the given service
|
|
117
|
+
# using the given pgt (proxy granting ticket); returns a ProxyTicket
|
|
118
|
+
# object.
|
|
119
|
+
#
|
|
120
|
+
# The pgt required to request a proxy ticket is obtained as part of
|
|
121
|
+
# a ValidationResponse.
|
|
122
|
+
def request_proxy_ticket(pgt, target_service)
|
|
123
|
+
uri = URI.parse(proxy_url)
|
|
124
|
+
h = uri.query ? query_to_hash(uri.query) : {}
|
|
125
|
+
h['pgt'] = pgt.ticket
|
|
126
|
+
h['targetService'] = target_service
|
|
127
|
+
uri.query = hash_to_query(h)
|
|
128
|
+
|
|
129
|
+
pr = request_cas_response(uri, ProxyResponse)
|
|
130
|
+
|
|
131
|
+
pt = ProxyTicket.new(pr.proxy_ticket, target_service)
|
|
132
|
+
pt.response = pr
|
|
133
|
+
|
|
134
|
+
return pt
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def retrieve_proxy_granting_ticket(pgt_iou)
|
|
138
|
+
uri = URI.parse(proxy_retrieval_url)
|
|
139
|
+
uri.query = (uri.query ? uri.query + "&" : "") + "pgtIou=#{CGI.escape(pgt_iou)}"
|
|
140
|
+
retrieve_url = uri.to_s
|
|
141
|
+
|
|
142
|
+
log.debug "Retrieving PGT for PGT IOU #{pgt_iou.inspect} from #{retrieve_url.inspect}"
|
|
143
|
+
|
|
144
|
+
# https = Net::HTTP.new(uri.host, uri.port)
|
|
145
|
+
# https.use_ssl = (uri.scheme == 'https')
|
|
146
|
+
# res = https.post(uri.path, ';')
|
|
147
|
+
uri = URI.parse(uri) unless uri.kind_of? URI
|
|
148
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
|
149
|
+
https.use_ssl = (uri.scheme == 'https')
|
|
150
|
+
res = https.start do |conn|
|
|
151
|
+
conn.get("#{uri.path}?#{uri.query}")
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
raise CASException, res.body unless res.kind_of? Net::HTTPSuccess
|
|
156
|
+
|
|
157
|
+
ProxyGrantingTicket.new(res.body.strip, pgt_iou)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def add_service_to_login_url(service_url)
|
|
161
|
+
uri = URI.parse(login_url)
|
|
162
|
+
# IU's CAS server can't deal with escaped redirects, and the param has to be named "casurl" not "service"
|
|
163
|
+
uri.query = (uri.query ? uri.query + "&" : "") + "casurl=#{service_url}"
|
|
164
|
+
uri.to_s
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
# Fetches a CAS response of the given type from the given URI.
|
|
169
|
+
# Type should be either ValidationResponse or ProxyResponse.
|
|
170
|
+
def request_cas_response(uri, type)
|
|
171
|
+
log.debug "Requesting CAS response form URI #{uri.inspect}"
|
|
172
|
+
|
|
173
|
+
uri = URI.parse(uri) unless uri.kind_of? URI
|
|
174
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
|
175
|
+
https.use_ssl = (uri.scheme == 'https')
|
|
176
|
+
raw_res = https.start do |conn|
|
|
177
|
+
conn.get("#{uri.path}?#{uri.query}")
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
#TODO: check to make sure that response code is 200 and handle errors otherwise
|
|
181
|
+
|
|
182
|
+
log.debug "CAS Responded with #{raw_res.inspect}:\n#{raw_res.body}"
|
|
183
|
+
|
|
184
|
+
type.new(raw_res.body)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Submits some data to the given URI and returns a Net::HTTPResponse.
|
|
188
|
+
def submit_data_to_cas(uri, data)
|
|
189
|
+
uri = URI.parse(uri) unless uri.kind_of? URI
|
|
190
|
+
req = Net::HTTP::Post.new(uri.path)
|
|
191
|
+
req.set_form_data(data, ';')
|
|
192
|
+
https = Net::HTTP.new(uri.host, uri.port)
|
|
193
|
+
https.use_ssl = (uri.scheme == 'https')
|
|
194
|
+
https.start {|conn| conn.request(req) }
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def query_to_hash(query)
|
|
198
|
+
CGI.parse(query)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def hash_to_query(hash)
|
|
202
|
+
pairs = []
|
|
203
|
+
hash.each do |k, vals|
|
|
204
|
+
vals = [vals] unless vals.kind_of? Array
|
|
205
|
+
# IU's CAS server doesn't accept escaped service URLs
|
|
206
|
+
vals.each {|v| pairs << "#{CGI.escape(k)}=#{v}"}
|
|
207
|
+
end
|
|
208
|
+
pairs.join("&")
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# The 'cas' strategy attempts to login users based on the CAS protocol
|
|
2
|
+
# http://www.ja-sig.org/products/cas/overview/background/index.html
|
|
3
|
+
#
|
|
4
|
+
# install the rubycas-client gem
|
|
5
|
+
# http://rubyforge.org/projects/rubycas-client/
|
|
6
|
+
#
|
|
7
|
+
require 'casclient'
|
|
8
|
+
class Merb::Authentication
|
|
9
|
+
module Strategies
|
|
10
|
+
class CAS < Merb::Authentication::Strategy
|
|
11
|
+
|
|
12
|
+
include CASClient
|
|
13
|
+
|
|
14
|
+
def run!
|
|
15
|
+
@client ||= Client.new(config)
|
|
16
|
+
|
|
17
|
+
service_ticket = read_ticket
|
|
18
|
+
|
|
19
|
+
cas_login_url = @client.add_service_to_login_url(service_url)
|
|
20
|
+
|
|
21
|
+
last_service_ticket = session[:cas_last_valid_ticket]
|
|
22
|
+
if (service_ticket && last_service_ticket &&
|
|
23
|
+
last_service_ticket.ticket == service_ticket.ticket &&
|
|
24
|
+
last_service_ticket.service == service_ticket.service)
|
|
25
|
+
|
|
26
|
+
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
|
27
|
+
# The only time when this is acceptable is if the user manually does a refresh and the ticket
|
|
28
|
+
# happens to be in the URL.
|
|
29
|
+
log.warn("Reusing previously validated ticket since the new ticket and service are the same.")
|
|
30
|
+
service_ticket = last_service_ticket
|
|
31
|
+
elsif last_service_ticket &&
|
|
32
|
+
!config[:authenticate_on_every_request] &&
|
|
33
|
+
session[@client.username_session_key]
|
|
34
|
+
# Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
|
|
35
|
+
# previously authenticated for this service). This is to prevent redirection to the CAS server on every
|
|
36
|
+
# request.
|
|
37
|
+
# This behaviour can be disabled (so that every request is routed through the CAS server) by setting
|
|
38
|
+
# the :authenticate_on_every_request config option to false.
|
|
39
|
+
log.debug "Existing local CAS session detected for #{session[@client.username_session_key].inspect}. "+
|
|
40
|
+
"Previous ticket #{last_service_ticket.ticket.inspect} will be re-used."
|
|
41
|
+
service_ticket = last_service_ticket
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
if service_ticket
|
|
45
|
+
@client.validate_service_ticket(service_ticket) unless service_ticket.has_been_validated?
|
|
46
|
+
validation_response = service_ticket.response
|
|
47
|
+
|
|
48
|
+
if service_ticket.is_valid?
|
|
49
|
+
log.info("Ticket #{service_ticket.inspect} for service #{service_ticket.service.inspect} " +
|
|
50
|
+
"belonging to user #{validation_response.user.inspect} is VALID.")
|
|
51
|
+
|
|
52
|
+
session[@client.username_session_key] = validation_response.user
|
|
53
|
+
session[@client.extra_attributes_session_key] = validation_response.extra_attributes
|
|
54
|
+
|
|
55
|
+
# Store the ticket in the session to avoid re-validating the same service
|
|
56
|
+
# ticket with the CAS server.
|
|
57
|
+
session[:cas_last_valid_ticket] = service_ticket
|
|
58
|
+
return true
|
|
59
|
+
else
|
|
60
|
+
log.warn("Ticket #{service_ticket.ticket.inspect} failed validation -- " +
|
|
61
|
+
"#{validation_response.failure_code}: #{validation_response.failure_message}")
|
|
62
|
+
redirect!(cas_login_url)
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
log.warn("No ticket -- redirecting to #{cas_login_url}")
|
|
67
|
+
redirect!(cas_login_url)
|
|
68
|
+
return false
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def read_ticket
|
|
73
|
+
ticket = request.params[:ticket]
|
|
74
|
+
|
|
75
|
+
return nil unless ticket
|
|
76
|
+
|
|
77
|
+
log.debug("Request contains ticket #{ticket.inspect}.")
|
|
78
|
+
|
|
79
|
+
if ticket =~ /^PT-/
|
|
80
|
+
ProxyTicket.new(ticket, service_url, request.params[:renew])
|
|
81
|
+
else
|
|
82
|
+
ServiceTicket.new(ticket, service_url, request.params[:renew])
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def service_url
|
|
87
|
+
if config[:service_url]
|
|
88
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
|
89
|
+
return config[:service_url]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
params = request.params.dup
|
|
93
|
+
params.delete(:ticket)
|
|
94
|
+
service_url = "#{request.protocol}://#{request.host}" + request.path
|
|
95
|
+
log.debug("Guessed service url: #{service_url.inspect}")
|
|
96
|
+
return service_url
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def config
|
|
100
|
+
::Merb::Plugins.config[:"rubycas-client"]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def log
|
|
104
|
+
::Merb.logger
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
end # CAS
|
|
108
|
+
end # Strategies
|
|
109
|
+
end
|
|
110
|
+
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
require 'pstore'
|
|
2
|
+
|
|
3
|
+
# Rails controller that responds to proxy generating ticket callbacks from the CAS server and allows
|
|
4
|
+
# for retrieval of those PGTs.
|
|
5
|
+
class CasProxyCallbackController < ActionController::Base
|
|
6
|
+
|
|
7
|
+
# Receives a proxy granting ticket from the CAS server and stores it in the database.
|
|
8
|
+
# Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole.
|
|
9
|
+
# In fact, the JA-SIG implementation of the CAS server will refuse to send PGTs to non-https URLs.
|
|
10
|
+
def receive_pgt
|
|
11
|
+
#FIXME: these checks don't work because REMOTE_HOST doesn't work consistently under all web servers (for example it doesn't work at all under mongrel)
|
|
12
|
+
# ... need to find a reliable way to check if the request came through from a reverse HTTPS proxy -- until then I'm disabling this check
|
|
13
|
+
#render_error "PGTs can be received only via HTTPS or local connections." and return unless
|
|
14
|
+
# request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
|
|
15
|
+
|
|
16
|
+
pgtIou = params['pgtIou']
|
|
17
|
+
|
|
18
|
+
# CAS Protocol spec says that the argument should be called 'pgt', but the JA-SIG CAS server seems to use pgtId.
|
|
19
|
+
# To accomodate this, we check for both parameters, although 'pgt' takes precedence over 'pgtId'.
|
|
20
|
+
pgtId = params['pgt'] || params['pgtId']
|
|
21
|
+
|
|
22
|
+
# We need to render a response with HTTP status code 200 when no pgtIou/pgtId is specified because CAS seems first
|
|
23
|
+
# call the action without any parameters (maybe to check if the server responds correctly)
|
|
24
|
+
render :text => "Okay, the server is up, but please specify a pgtIou and pgtId." and return unless pgtIou and pgtId
|
|
25
|
+
|
|
26
|
+
# TODO: pstore contents should probably be encrypted...
|
|
27
|
+
pstore = open_pstore
|
|
28
|
+
|
|
29
|
+
pstore.transaction do
|
|
30
|
+
pstore[pgtIou] = pgtId
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
render :text => "PGT received. Thank you!" and return
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Retreives a proxy granting ticket, sends it to output, and deletes the pgt from session storage.
|
|
37
|
+
# Note that this action should ALWAYS be called via https, otherwise you have a gaping security hole --
|
|
38
|
+
# in fact, the action will not work if the request is not made via SSL or is not local (we allow for local
|
|
39
|
+
# non-SSL requests since this allows for the use of reverse HTTPS proxies like Pound).
|
|
40
|
+
def retrieve_pgt
|
|
41
|
+
#render_error "You can only retrieve PGTs via HTTPS or local connections." and return unless
|
|
42
|
+
# request.ssl? or request.env['REMOTE_HOST'] == "127.0.0.1"
|
|
43
|
+
|
|
44
|
+
pgtIou = params['pgtIou']
|
|
45
|
+
|
|
46
|
+
render_error "No pgtIou specified. Cannot retreive the pgtId." and return unless pgtIou
|
|
47
|
+
|
|
48
|
+
pstore = open_pstore
|
|
49
|
+
|
|
50
|
+
pgt = nil
|
|
51
|
+
pstore.transaction do
|
|
52
|
+
pgt = pstore[pgtIou]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if not pgt
|
|
56
|
+
render_error "Invalid pgtIou specified. Perhaps this pgt has already been retrieved?" and return
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
render :text => pgt
|
|
60
|
+
|
|
61
|
+
# TODO: need to periodically clean the storage, otherwise it will just keep growing
|
|
62
|
+
pstore.transaction do
|
|
63
|
+
pstore.delete pgtIou
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
def render_error(msg)
|
|
69
|
+
# Note that the error messages are mostly just for debugging, since the CAS server never reads them.
|
|
70
|
+
render :text => msg, :status => 500
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def open_pstore
|
|
74
|
+
PStore.new("#{RAILS_ROOT}/tmp/cas_pgt.pstore")
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
module CASClient
|
|
2
|
+
module Frameworks
|
|
3
|
+
module Rails
|
|
4
|
+
class Filter
|
|
5
|
+
cattr_reader :config, :log, :client
|
|
6
|
+
|
|
7
|
+
# These are initialized when you call configure.
|
|
8
|
+
@@config = nil
|
|
9
|
+
@@client = nil
|
|
10
|
+
@@log = nil
|
|
11
|
+
|
|
12
|
+
class << self
|
|
13
|
+
def filter(controller)
|
|
14
|
+
raise "Cannot use the CASClient filter because it has not yet been configured." if config.nil?
|
|
15
|
+
|
|
16
|
+
st = read_ticket(controller)
|
|
17
|
+
|
|
18
|
+
lst = controller.session[:cas_last_valid_ticket]
|
|
19
|
+
|
|
20
|
+
if st && lst && lst.ticket == st.ticket && lst.service == st.service
|
|
21
|
+
# warn() rather than info() because we really shouldn't be re-validating the same ticket.
|
|
22
|
+
# The only time when this is acceptable is if the user manually does a refresh and the ticket
|
|
23
|
+
# happens to be in the URL.
|
|
24
|
+
log.warn("Re-using previously validated ticket since the new ticket and service are the same.")
|
|
25
|
+
st = lst
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
if st
|
|
29
|
+
client.validate_service_ticket(st) unless st.has_been_validated?
|
|
30
|
+
vr = st.response
|
|
31
|
+
|
|
32
|
+
if st.is_valid?
|
|
33
|
+
log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
|
|
34
|
+
controller.session[client.username_session_key] = vr.user
|
|
35
|
+
controller.session[client.extra_attributes_session_key] = vr.extra_attributes
|
|
36
|
+
|
|
37
|
+
# RubyCAS-Client 1.x used :casfilteruser as it's username session key,
|
|
38
|
+
# so we need to set this here to ensure compatibility with configurations
|
|
39
|
+
# built around the old client.
|
|
40
|
+
controller.session[:casfilteruser] = vr.user
|
|
41
|
+
|
|
42
|
+
# Store the ticket in the session to avoid re-validating the same service
|
|
43
|
+
# ticket with the CAS server.
|
|
44
|
+
controller.session[:cas_last_valid_ticket] = st
|
|
45
|
+
|
|
46
|
+
if vr.pgt_iou
|
|
47
|
+
log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
|
|
48
|
+
pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
|
|
49
|
+
if pgt
|
|
50
|
+
log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
|
|
51
|
+
controller.session[:cas_pgt] = pgt
|
|
52
|
+
# For backwards compatibility with RubyCAS-Client 1.x configurations...
|
|
53
|
+
controller.session[:casfilterpgt] = pgt
|
|
54
|
+
else
|
|
55
|
+
log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return true
|
|
60
|
+
else
|
|
61
|
+
log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
|
|
62
|
+
redirect_to_cas_for_authentication(controller)
|
|
63
|
+
return false
|
|
64
|
+
end
|
|
65
|
+
elsif !config[:authenticate_on_every_request] && controller.session[client.username_session_key]
|
|
66
|
+
# Don't re-authenticate with the CAS server if we already previously authenticated and the
|
|
67
|
+
# :authenticate_on_every_request option is disabled (it's disabled by default).
|
|
68
|
+
log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
|
|
69
|
+
"User will not be re-authenticated."
|
|
70
|
+
return true
|
|
71
|
+
else
|
|
72
|
+
if returning_from_gateway?(controller)
|
|
73
|
+
log.info "Returning from CAS gateway without authentication."
|
|
74
|
+
|
|
75
|
+
if use_gatewaying?
|
|
76
|
+
log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
|
|
77
|
+
return true
|
|
78
|
+
else
|
|
79
|
+
log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
redirect_to_cas_for_authentication(controller)
|
|
84
|
+
return false
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def configure(config)
|
|
89
|
+
@@config = config
|
|
90
|
+
@@config[:logger] = RAILS_DEFAULT_LOGGER unless @@config[:logger]
|
|
91
|
+
@@client = CASClient::Client.new(config)
|
|
92
|
+
@@log = client.log
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def use_gatewaying?
|
|
96
|
+
@@config[:use_gatewaying]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def returning_from_gateway?(controller)
|
|
100
|
+
controller.session[:cas_sent_to_gateway]
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def redirect_to_cas_for_authentication(controller)
|
|
104
|
+
service_url = read_service_url(controller)
|
|
105
|
+
redirect_url = client.add_service_to_login_url(service_url)
|
|
106
|
+
|
|
107
|
+
if use_gatewaying?
|
|
108
|
+
controller.session[:cas_sent_to_gateway] = true
|
|
109
|
+
redirect_url << "&gateway=true"
|
|
110
|
+
else
|
|
111
|
+
controller.session[:cas_sent_to_gateway] = false
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
log.debug("Redirecting to #{redirect_url.inspect}")
|
|
115
|
+
controller.send(:redirect_to, redirect_url)
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
def read_ticket(controller)
|
|
120
|
+
ticket = controller.params[:casticket]
|
|
121
|
+
|
|
122
|
+
return nil unless ticket
|
|
123
|
+
|
|
124
|
+
log.debug("Request contains ticket #{ticket.inspect}.")
|
|
125
|
+
|
|
126
|
+
if ticket =~ /^PT-/
|
|
127
|
+
ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
|
128
|
+
else
|
|
129
|
+
ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def read_service_url(controller)
|
|
134
|
+
if config[:service_url]
|
|
135
|
+
log.debug("Using explicitly set service url: #{config[:service_url]}")
|
|
136
|
+
return config[:service_url]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
params = controller.params.dup
|
|
140
|
+
params.delete(:casticket)
|
|
141
|
+
service_url = controller.url_for(params)
|
|
142
|
+
log.debug("Guessed service url: #{service_url.inspect}")
|
|
143
|
+
return service_url
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Creates a file in tmp/sessions linking a SessionTicket
|
|
147
|
+
# with the local Rails session id. The file is named
|
|
148
|
+
# cas_sess.<session ticket> and its text contents is the corresponding
|
|
149
|
+
# Rails session id.
|
|
150
|
+
# Returns the filename of the lookup file created.
|
|
151
|
+
def store_service_session_lookup(st, sid)
|
|
152
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
153
|
+
f = File.new(filename_of_service_session_lookup(st), 'w')
|
|
154
|
+
f.write(sid)
|
|
155
|
+
f.close
|
|
156
|
+
return f.path
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Returns the local Rails session ID corresponding to the given
|
|
160
|
+
# ServiceTicket. This is done by reading the contents of the
|
|
161
|
+
# cas_sess.<session ticket> file created in a prior call to
|
|
162
|
+
# #store_service_session_lookup.
|
|
163
|
+
def read_service_session_lookup(st)
|
|
164
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
165
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
|
166
|
+
return File.exists?(ssl_filename) && IO.read(ssl_filename)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Removes a stored relationship between a ServiceTicket and a local
|
|
170
|
+
# Rails session id. This should be called when the session is being
|
|
171
|
+
# closed.
|
|
172
|
+
#
|
|
173
|
+
# See #store_service_session_lookup.
|
|
174
|
+
def delete_service_session_lookup(st)
|
|
175
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
176
|
+
ssl_filename = filename_of_service_session_lookup(st)
|
|
177
|
+
File.delete(ssl_filename) if File.exists?(ssl_filename)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Returns the path and filename of the service session lookup file.
|
|
181
|
+
def filename_of_service_session_lookup(st)
|
|
182
|
+
st = st.ticket if st.kind_of? ServiceTicket
|
|
183
|
+
return "#{RAILS_ROOT}/tmp/sessions/cas_sess.#{st}"
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
class GatewayFilter < Filter
|
|
189
|
+
def self.use_gatewaying?
|
|
190
|
+
return true unless @@config[:use_gatewaying] == false
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|