gunark-rubycas-client 2.0.99

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.
@@ -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