gunark-rubycas-client 2.0.99

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,313 @@
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
+ last_st = controller.session[:cas_last_valid_ticket]
17
+
18
+ if single_sign_out(controller)
19
+ controller.send(:render, :text => "CAS Single-Sign-Out request intercepted.")
20
+ return false
21
+ end
22
+
23
+ st = read_ticket(controller)
24
+
25
+ is_new_session = true
26
+
27
+ if st && last_st &&
28
+ last_st.ticket == st.ticket &&
29
+ last_st.service == st.service
30
+ # warn() rather than info() because we really shouldn't be re-validating the same ticket.
31
+ # The only situation where this is acceptable is if the user manually does a refresh and
32
+ # the same ticket happens to be in the URL.
33
+ log.warn("Re-using previously validated ticket since the ticket id and service are the same.")
34
+ st = last_st
35
+ is_new_session = false
36
+ elsif last_st &&
37
+ !config[:authenticate_on_every_request] &&
38
+ controller.session[client.username_session_key]
39
+ # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already
40
+ # previously authenticated for this service). This is to prevent redirection to the CAS server on every
41
+ # request.
42
+ # This behaviour can be disabled (so that every request is routed through the CAS server) by setting
43
+ # the :authenticate_on_every_request config option to false.
44
+ log.debug "Existing local CAS session detected for #{controller.session[client.username_session_key].inspect}. "+
45
+ "Previous ticket #{last_st.ticket.inspect} will be re-used."
46
+ st = last_st
47
+ is_new_session = false
48
+ end
49
+
50
+ if st
51
+ client.validate_service_ticket(st) unless st.has_been_validated?
52
+ vr = st.response
53
+
54
+ if st.is_valid?
55
+ if is_new_session
56
+ log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{vr.user.inspect} is VALID.")
57
+ controller.session[client.username_session_key] = vr.user.dup
58
+ controller.session[client.extra_attributes_session_key] = HashWithIndifferentAccess.new(vr.extra_attributes.dup)
59
+
60
+ if vr.extra_attributes
61
+ log.debug("Extra user attributes provided along with ticket #{st.ticket.inspect}: #{vr.extra_attributes.inspect}.")
62
+ end
63
+
64
+ # RubyCAS-Client 1.x used :casfilteruser as it's username session key,
65
+ # so we need to set this here to ensure compatibility with configurations
66
+ # built around the old client.
67
+ controller.session[:casfilteruser] = vr.user
68
+
69
+ f = store_service_session_lookup(st, controller.session.session_id)
70
+ log.debug("Wrote service session lookup file to #{f.inspect} with session id #{controller.session.session_id.inspect}.")
71
+ end
72
+
73
+ # Store the ticket in the session to avoid re-validating the same service
74
+ # ticket with the CAS server.
75
+ controller.session[:cas_last_valid_ticket] = st
76
+
77
+ if vr.pgt_iou
78
+ unless controller.session[:cas_pgt] && controller.session[:cas_pgt].ticket && controller.session[:cas_pgt].iou == vr.pgt_iou
79
+ log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...")
80
+ pgt = client.retrieve_proxy_granting_ticket(vr.pgt_iou)
81
+
82
+ if pgt
83
+ log.debug("Got PGT #{pgt.ticket.inspect} for PGT IOU #{pgt.iou.inspect}. This will be stored in the session.")
84
+ controller.session[:cas_pgt] = pgt
85
+ # For backwards compatibility with RubyCAS-Client 1.x configurations...
86
+ controller.session[:casfilterpgt] = pgt
87
+ else
88
+ log.error("Failed to retrieve a PGT for PGT IOU #{vr.pgt_iou}!")
89
+ end
90
+ else
91
+ log.info("PGT is present in session and PGT IOU #{vr.pgt_iou} matches the saved PGT IOU. Not retrieving new PGT.")
92
+ end
93
+
94
+ end
95
+
96
+ return true
97
+ else
98
+ log.warn("Ticket #{st.ticket.inspect} failed validation -- #{vr.failure_code}: #{vr.failure_message}")
99
+ redirect_to_cas_for_authentication(controller)
100
+ return false
101
+ end
102
+ else
103
+ if returning_from_gateway?(controller)
104
+ log.info "Returning from CAS gateway without authentication."
105
+
106
+ if use_gatewaying?
107
+ log.info "This CAS client is configured to use gatewaying, so we will permit the user to continue without authentication."
108
+ return true
109
+ else
110
+ log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!"
111
+ end
112
+ end
113
+
114
+ redirect_to_cas_for_authentication(controller)
115
+ return false
116
+ end
117
+ end
118
+
119
+ def configure(config)
120
+ @@config = config
121
+ @@config[:logger] = RAILS_DEFAULT_LOGGER unless @@config[:logger]
122
+ @@client = CASClient::Client.new(config)
123
+ @@log = client.log
124
+ end
125
+
126
+ def use_gatewaying?
127
+ @@config[:use_gatewaying]
128
+ end
129
+
130
+ # Clears the given controller's local Rails session, does some local
131
+ # CAS cleanup, and redirects to the CAS logout page. Additionally, the
132
+ # <tt>request.referer</tt> value from the <tt>controller</tt> instance
133
+ # is passed to the CAS server as a 'destination' parameter. This
134
+ # allows RubyCAS server to provide a follow-up login page allowing
135
+ # the user to log back in to the service they just logged out from
136
+ # using a different username and password. Other CAS server
137
+ # implemenations may use this 'destination' parameter in different
138
+ # ways.
139
+ # If given, the optional <tt>service</tt> URL overrides
140
+ # <tt>request.referer</tt>.
141
+ def logout(controller, service = nil)
142
+ referer = service || controller.request.referer
143
+ st = controller.session[:cas_last_valid_ticket]
144
+ delete_service_session_lookup(st) if st
145
+ controller.send(:reset_session)
146
+ controller.send(:redirect_to, client.logout_url(referer))
147
+ end
148
+
149
+ def redirect_to_cas_for_authentication(controller)
150
+ service_url = read_service_url(controller)
151
+ redirect_url = client.add_service_to_login_url(service_url)
152
+
153
+ if use_gatewaying?
154
+ controller.session[:cas_sent_to_gateway] = true
155
+ redirect_url << "&gateway=true"
156
+ else
157
+ controller.session[:cas_sent_to_gateway] = false
158
+ end
159
+
160
+ if controller.session[:previous_redirect_to_cas] &&
161
+ controller.session[:previous_redirect_to_cas] > (Time.now - 1.second)
162
+ log.warn("Previous redirect to the CAS server was less than a second ago. The client at #{controller.request.remote_ip.inspect} may be stuck in a redirection loop!")
163
+ controller.session[:cas_validation_retry_count] ||= 0
164
+
165
+ if controller.session[:cas_validation_retry_count] > 3
166
+ log.error("Redirection loop intercepted. Client at #{controller.request.remote_ip.inspect} will be redirected back to login page and forced to renew authentication.")
167
+ redirect_url += "&renew=1&redirection_loop_intercepted=1"
168
+ end
169
+
170
+ controller.session[:cas_validation_retry_count] += 1
171
+ else
172
+ controller.session[:cas_validation_retry_count] = 0
173
+ end
174
+ controller.session[:previous_redirect_to_cas] = Time.now
175
+
176
+ log.debug("Redirecting to #{redirect_url.inspect}")
177
+ controller.send(:redirect_to, redirect_url)
178
+ end
179
+
180
+ private
181
+ def single_sign_out(controller)
182
+
183
+ # Avoid calling raw_post (which may consume the post body) if
184
+ # this seems to be a file upload
185
+ if content_type = controller.request.headers["CONTENT_TYPE"] &&
186
+ content_type =~ %r{^multipart/}
187
+ return false
188
+ end
189
+
190
+ if controller.request.post? &&
191
+ controller.params['logoutRequest'] &&
192
+ controller.params['logoutRequest'] =~
193
+ %r{^<samlp:LogoutRequest.*?<samlp:SessionIndex>(.*)</samlp:SessionIndex>}m
194
+ # TODO: Maybe check that the request came from the registered CAS server? Although this might be
195
+ # pointless since it's easily spoofable...
196
+ si = $~[1]
197
+ log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}."
198
+
199
+ required_sess_store = CGI::Session::ActiveRecordStore
200
+ current_sess_store = ActionController::Base.session_options[:database_manager]
201
+
202
+ if current_sess_store == required_sess_store
203
+ session_id = read_service_session_lookup(si)
204
+
205
+ if session_id
206
+ session = CGI::Session::ActiveRecordStore::Session.find_by_session_id(session_id)
207
+ if session
208
+ session.destroy
209
+ log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{si.inspect}.")
210
+ else
211
+ log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
212
+ end
213
+
214
+ log.info("Single-sign-out for session #{session_id.inspect} completed successfuly.")
215
+ else
216
+ log.warn("Couldn't destroy session with SessionIndex #{si} because no corresponding session id could be looked up.")
217
+ end
218
+ else
219
+ log.error "Cannot process logout request because this Rails application's session store is "+
220
+ " #{current_sess_store.name.inspect}. Single Sign-Out only works with the "+
221
+ " #{required_sess_store.name.inspect} session store."
222
+ end
223
+
224
+ # Return true to indicate that a single-sign-out request was detected
225
+ # and that further processing of the request is unnecessary.
226
+ return true
227
+ end
228
+
229
+ # This is not a single-sign-out request.
230
+ return false
231
+ end
232
+
233
+ def read_ticket(controller)
234
+ ticket = controller.params[:ticket]
235
+
236
+ return nil unless ticket
237
+
238
+ log.debug("Request contains ticket #{ticket.inspect}.")
239
+
240
+ if ticket =~ /^PT-/
241
+ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
242
+ else
243
+ ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
244
+ end
245
+ end
246
+
247
+ def returning_from_gateway?(controller)
248
+ controller.session[:cas_sent_to_gateway]
249
+ end
250
+
251
+ def read_service_url(controller)
252
+ if config[:service_url]
253
+ log.debug("Using explicitly set service url: #{config[:service_url]}")
254
+ return config[:service_url]
255
+ end
256
+
257
+ params = controller.params.dup
258
+ params.delete(:ticket)
259
+ service_url = controller.url_for(params)
260
+ log.debug("Guessed service url: #{service_url.inspect}")
261
+ return service_url
262
+ end
263
+
264
+ # Creates a file in tmp/sessions linking a SessionTicket
265
+ # with the local Rails session id. The file is named
266
+ # cas_sess.<session ticket> and its text contents is the corresponding
267
+ # Rails session id.
268
+ # Returns the filename of the lookup file created.
269
+ def store_service_session_lookup(st, sid)
270
+ st = st.ticket if st.kind_of? ServiceTicket
271
+ f = File.new(filename_of_service_session_lookup(st), 'w')
272
+ f.write(sid)
273
+ f.close
274
+ return filename_of_service_session_lookup(st)
275
+ end
276
+
277
+ # Returns the local Rails session ID corresponding to the given
278
+ # ServiceTicket. This is done by reading the contents of the
279
+ # cas_sess.<session ticket> file created in a prior call to
280
+ # #store_service_session_lookup.
281
+ def read_service_session_lookup(st)
282
+ st = st.ticket if st.kind_of? ServiceTicket
283
+ ssl_filename = filename_of_service_session_lookup(st)
284
+ return File.exists?(ssl_filename) && IO.read(ssl_filename)
285
+ end
286
+
287
+ # Removes a stored relationship between a ServiceTicket and a local
288
+ # Rails session id. This should be called when the session is being
289
+ # closed.
290
+ #
291
+ # See #store_service_session_lookup.
292
+ def delete_service_session_lookup(st)
293
+ st = st.ticket if st.kind_of? ServiceTicket
294
+ ssl_filename = filename_of_service_session_lookup(st)
295
+ File.delete(ssl_filename) if File.exists?(ssl_filename)
296
+ end
297
+
298
+ # Returns the path and filename of the service session lookup file.
299
+ def filename_of_service_session_lookup(st)
300
+ st = st.ticket if st.kind_of? ServiceTicket
301
+ return "#{RAILS_ROOT}/tmp/sessions/cas_sess.#{st}"
302
+ end
303
+ end
304
+ end
305
+
306
+ class GatewayFilter < Filter
307
+ def self.use_gatewaying?
308
+ return true unless @@config[:use_gatewaying] == false
309
+ end
310
+ end
311
+ end
312
+ end
313
+ end
@@ -0,0 +1,185 @@
1
+ module CASClient
2
+ module XmlResponse
3
+ attr_reader :xml, :parse_datetime
4
+ attr_reader :failure_code, :failure_message
5
+
6
+ def check_and_parse_xml(raw_xml)
7
+ begin
8
+ doc = REXML::Document.new(raw_xml)
9
+ rescue REXML::ParseException => e
10
+ raise BadResponseException,
11
+ "MALFORMED CAS RESPONSE:\n#{raw_xml.inspect}\n\nEXCEPTION:\n#{e}"
12
+ end
13
+
14
+ unless doc.elements && doc.elements["cas:serviceResponse"]
15
+ raise BadResponseException,
16
+ "This does not appear to be a valid CAS response (missing cas:serviceResponse root element)!\nXML DOC:\n#{doc.to_s}"
17
+ end
18
+
19
+ return doc.elements["cas:serviceResponse"].elements[1]
20
+ end
21
+
22
+ def to_s
23
+ xml.to_s
24
+ end
25
+ end
26
+
27
+ # Represents a response from the CAS server to a 'validate' request
28
+ # (i.e. after validating a service/proxy ticket).
29
+ class ValidationResponse
30
+ include XmlResponse
31
+
32
+ attr_reader :protocol, :user, :pgt_iou, :proxies, :extra_attributes
33
+
34
+ def initialize(raw_text)
35
+ parse(raw_text)
36
+ end
37
+
38
+ def parse(raw_text)
39
+ raise BadResponseException,
40
+ "CAS response is empty/blank." if raw_text.blank?
41
+ @parse_datetime = Time.now
42
+
43
+ if raw_text =~ /^(yes|no)\n(.*?)\n$/m
44
+ @protocol = 1.0
45
+ @valid = $~[1] == 'yes'
46
+ @user = $~[2]
47
+ return
48
+ end
49
+
50
+ @xml = check_and_parse_xml(raw_text)
51
+
52
+ # if we got this far then we've got a valid XML response, so we're doing CAS 2.0
53
+ @protocol = 2.0
54
+
55
+ if is_success?
56
+ @user = @xml.elements["cas:user"].text.strip if @xml.elements["cas:user"]
57
+ @pgt_iou = @xml.elements["cas:proxyGrantingTicket"].text.strip if @xml.elements["cas:proxyGrantingTicket"]
58
+
59
+ proxy_els = @xml.elements.to_a('//cas:authenticationSuccess/cas:proxies/cas:proxy')
60
+ if proxy_els.size > 0
61
+ @proxies = []
62
+ proxy_els.each do |el|
63
+ @proxies << el.text
64
+ end
65
+ end
66
+
67
+ @extra_attributes = {}
68
+ @xml.elements.to_a('//cas:authenticationSuccess/*').each do |el|
69
+ @extra_attributes.merge!(Hash.from_xml(el.to_s)) unless el.prefix == 'cas'
70
+ end
71
+
72
+ # unserialize extra attributes
73
+ @extra_attributes.each do |k, v|
74
+ @extra_attributes[k] = YAML.load(v)
75
+ end
76
+ elsif is_failure?
77
+ @failure_code = @xml.elements['//cas:authenticationFailure'].attributes['code']
78
+ @failure_message = @xml.elements['//cas:authenticationFailure'].text.strip
79
+ else
80
+ # this should never happen, since the response should already have been recognized as invalid
81
+ raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
82
+ end
83
+
84
+ end
85
+
86
+ def is_success?
87
+ @valid == true || (protocol > 1.0 && xml.name == "authenticationSuccess")
88
+ end
89
+
90
+ def is_failure?
91
+ @valid == false || (protocol > 1.0 && xml.name == "authenticationFailure" )
92
+ end
93
+ end
94
+
95
+ # Represents a response from the CAS server to a proxy ticket request
96
+ # (i.e. after requesting a proxy ticket).
97
+ class ProxyResponse
98
+ include XmlResponse
99
+
100
+ attr_reader :proxy_ticket
101
+
102
+ def initialize(raw_text)
103
+ parse(raw_text)
104
+ end
105
+
106
+ def parse(raw_text)
107
+ raise BadResponseException,
108
+ "CAS response is empty/blank." if raw_text.blank?
109
+ @parse_datetime = Time.now
110
+
111
+ @xml = check_and_parse_xml(raw_text)
112
+
113
+ if is_success?
114
+ @proxy_ticket = @xml.elements["cas:proxyTicket"].text.strip if @xml.elements["cas:proxyTicket"]
115
+ elsif is_failure?
116
+ @failure_code = @xml.elements['//cas:proxyFailure'].attributes['code']
117
+ @failure_message = @xml.elements['//cas:proxyFailure'].text.strip
118
+ else
119
+ # this should never happen, since the response should already have been recognized as invalid
120
+ raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
121
+ end
122
+
123
+ end
124
+
125
+ def is_success?
126
+ xml.name == "proxySuccess"
127
+ end
128
+
129
+ def is_failure?
130
+ xml.name == "proxyFailure"
131
+ end
132
+ end
133
+
134
+ # Represents a response from the CAS server to a login request
135
+ # (i.e. after submitting a username/password).
136
+ class LoginResponse
137
+ attr_reader :tgt, :ticket, :service_redirect_url
138
+ attr_reader :failure_message
139
+
140
+ def initialize(http_response = nil)
141
+ parse_http_response(http_response) if http_response
142
+ end
143
+
144
+ def parse_http_response(http_response)
145
+ header = http_response.to_hash
146
+
147
+ # FIXME: this regexp might be incorrect...
148
+ if header['set-cookie'] &&
149
+ header['set-cookie'].first &&
150
+ header['set-cookie'].first =~ /tgt=([^&]+);/
151
+ @tgt = $~[1]
152
+ end
153
+
154
+ location = header['location'].first if header['location'] && header['location'].first
155
+ if location =~ /ticket=([^&]+)/
156
+ @ticket = $~[1]
157
+ end
158
+
159
+ if !http_response.kind_of?(Net::HTTPSuccess) || ticket.blank?
160
+ @failure = true
161
+ # Try to extract the error message -- this only works with RubyCAS-Server.
162
+ # For other servers we just return the entire response body (i.e. the whole error page).
163
+ body = http_response.body
164
+ if body =~ /<div class="messagebox mistake">(.*?)<\/div>/m
165
+ @failure_message = $~[1].strip
166
+ else
167
+ @failure_message = body
168
+ end
169
+ end
170
+
171
+ @service_redirect_url = location
172
+ end
173
+
174
+ def is_success?
175
+ !@failure && !ticket.blank?
176
+ end
177
+
178
+ def is_failure?
179
+ @failure == true
180
+ end
181
+ end
182
+
183
+ class BadResponseException < CASException
184
+ end
185
+ end