(.*)}m}
293 | # TODO: Maybe check that the request came from the registered CAS server? Although this might be
294 | # pointless since it's easily spoofable...
295 | si = $~[1]
296 |
297 | unless config[:enable_single_sign_out]
298 | log.warn "Ignoring single-sign-out request for CAS session #{si.inspect} because ssout functionality is not enabled (see the :enable_single_sign_out config option)."
299 | return false
300 | end
301 |
302 | log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}."
303 |
304 | @@client.ticket_store.process_single_sign_out(si)
305 |
306 | # Return true to indicate that a single-sign-out request was detected
307 | # and that further processing of the request is unnecessary.
308 | return true
309 | end
310 |
311 | # This is not a single-sign-out request.
312 | return false
313 | end
314 |
315 | def read_ticket(controller)
316 | ticket = controller.params[:ticket]
317 |
318 | return nil unless ticket
319 |
320 | log.debug("Request contains ticket #{ticket.inspect}.")
321 |
322 | if ticket =~ /^PT-/
323 | ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew])
324 | else
325 | ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew])
326 | end
327 | end
328 |
329 | def returning_from_gateway?(controller)
330 | controller.session[:cas_sent_to_gateway]
331 | end
332 |
333 | def read_service_url(controller)
334 | if config[:service_url]
335 | log.debug("Using explicitly set service url: #{config[:service_url]}")
336 | return config[:service_url]
337 | end
338 |
339 | params = {}.with_indifferent_access
340 | params.update(controller.request.query_parameters)
341 | params.update(controller.request.path_parameters)
342 | params.delete(:ticket)
343 | service_url = controller.url_for(params)
344 | log.debug("Guessed service url: #{service_url.inspect}")
345 | return service_url
346 | end
347 | end
348 | end
349 |
350 | class GatewayFilter < Filter
351 | def self.use_gatewaying?
352 | return true unless @@config[:use_gatewaying] == false
353 | end
354 | end
355 | end
356 | end
357 | end
358 |
--------------------------------------------------------------------------------
/lib/casclient/responses.rb:
--------------------------------------------------------------------------------
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, :raw => :all)
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, options={})
35 | parse(raw_text, options)
36 | end
37 |
38 | def parse(raw_text, options)
39 | raise BadResponseException,
40 | "CAS response is empty/blank." if raw_text.to_s.empty?
41 | @parse_datetime = Time.now
42 | if raw_text =~ /^(yes|no)\n(.*?)\n$/m
43 | @protocol = 1.0
44 | @valid = $~[1] == 'yes'
45 | @user = $~[2]
46 | return
47 | end
48 |
49 | @xml = check_and_parse_xml(raw_text)
50 |
51 | # if we got this far then we've got a valid XML response, so we're doing CAS 2.0
52 | @protocol = 2.0
53 |
54 | if is_success?
55 | cas_user = @xml.elements["cas:user"]
56 | @user = cas_user.text.strip if 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/cas:attributes/* | //cas:authenticationSuccess/*[local-name() != \'proxies\' and local-name() != \'proxyGrantingTicket\' and local-name() != \'user\' and local-name() != \'attributes\']').each do |el|
69 | inner_text = el.cdatas.length > 0 ? el.cdatas.join('') : el.text
70 | name = el.name
71 | unless (attrs = el.attributes).empty?
72 | name = attrs['name']
73 | inner_text = attrs['value']
74 | end
75 | @extra_attributes.merge! name => inner_text
76 | end
77 |
78 | # unserialize extra attributes
79 | @extra_attributes.each do |k, v|
80 | @extra_attributes[k] = parse_extra_attribute_value(v, options[:encode_extra_attributes_as])
81 | end
82 | elsif is_failure?
83 | @failure_code = @xml.elements['//cas:authenticationFailure'].attributes['code']
84 | @failure_message = @xml.elements['//cas:authenticationFailure'].text.strip
85 | else
86 | # this should never happen, since the response should already have been recognized as invalid
87 | raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
88 | end
89 | end
90 |
91 | def parse_extra_attribute_value(value, encode_extra_attributes_as)
92 | attr_value = if value.to_s.empty?
93 | nil
94 | elsif !encode_extra_attributes_as
95 | begin
96 | YAML.load(value)
97 | rescue ArgumentError => e
98 | raise ArgumentError, "Error parsing extra attribute with value #{value} as YAML: #{e}"
99 | end
100 | else
101 | if encode_extra_attributes_as == :json
102 | begin
103 | JSON.parse(value)
104 | rescue JSON::ParserError
105 | value
106 | end
107 | elsif encode_extra_attributes_as == :raw
108 | value
109 | else
110 | YAML.load(value)
111 | end
112 | end
113 |
114 | unless attr_value.kind_of?(Enumerable) || attr_value.kind_of?(TrueClass) || attr_value.kind_of?(FalseClass) || attr_value.nil?
115 | attr_value.to_s
116 | else
117 | attr_value
118 | end
119 | end
120 |
121 | def is_success?
122 | (instance_variable_defined?(:@valid) && @valid) || (protocol > 1.0 && xml.name == "authenticationSuccess")
123 | end
124 |
125 | def is_failure?
126 | (instance_variable_defined?(:@valid) && !@valid) || (protocol > 1.0 && xml.name == "authenticationFailure" )
127 | end
128 | end
129 |
130 | # Represents a response from the CAS server to a proxy ticket request
131 | # (i.e. after requesting a proxy ticket).
132 | class ProxyResponse
133 | include XmlResponse
134 |
135 | attr_reader :proxy_ticket
136 |
137 | def initialize(raw_text, options={})
138 | parse(raw_text)
139 | end
140 |
141 | def parse(raw_text)
142 | raise BadResponseException,
143 | "CAS response is empty/blank." if raw_text.to_s.empty?
144 | @parse_datetime = Time.now
145 |
146 | @xml = check_and_parse_xml(raw_text)
147 |
148 | if is_success?
149 | @proxy_ticket = @xml.elements["cas:proxyTicket"].text.strip if @xml.elements["cas:proxyTicket"]
150 | elsif is_failure?
151 | @failure_code = @xml.elements['//cas:proxyFailure'].attributes['code']
152 | @failure_message = @xml.elements['//cas:proxyFailure'].text.strip
153 | else
154 | # this should never happen, since the response should already have been recognized as invalid
155 | raise BadResponseException, "BAD CAS RESPONSE:\n#{raw_text.inspect}\n\nXML DOC:\n#{doc.inspect}"
156 | end
157 |
158 | end
159 |
160 | def is_success?
161 | xml.name == "proxySuccess"
162 | end
163 |
164 | def is_failure?
165 | xml.name == "proxyFailure"
166 | end
167 | end
168 |
169 | # Represents a response from the CAS server to a login request
170 | # (i.e. after submitting a username/password).
171 | class LoginResponse
172 | attr_reader :tgt, :ticket, :service_redirect_url
173 | attr_reader :failure_message
174 |
175 | def initialize(http_response = nil, options={})
176 | parse_http_response(http_response) if http_response
177 | end
178 |
179 | def parse_http_response(http_response)
180 | header = http_response.to_hash
181 |
182 | # FIXME: this regexp might be incorrect...
183 | if header['set-cookie'] &&
184 | header['set-cookie'].first &&
185 | header['set-cookie'].first =~ /tgt=([^&]+);/
186 | @tgt = $~[1]
187 | end
188 |
189 | location = header['location'].first if header['location'] && header['location'].first
190 | if location =~ /ticket=([^&]+)/
191 | @ticket = $~[1]
192 | end
193 |
194 | # Legacy check. CAS Server used to return a 200 (Success) or a 302 (Found) on successful authentication.
195 | # This behavior should be deprecated at some point in the future.
196 | legacy_valid_ticket = (http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present?
197 |
198 | # If using rubycas-server 1.1.0+
199 | valid_ticket = http_response.kind_of?(Net::HTTPSeeOther) && @ticket.present?
200 |
201 | if !legacy_valid_ticket && !valid_ticket
202 | @failure = true
203 | # Try to extract the error message -- this only works with RubyCAS-Server.
204 | # For other servers we just return the entire response body (i.e. the whole error page).
205 | body = http_response.body
206 | if body =~ /(.*?)<\/div>/m
207 | @failure_message = $~[1].strip
208 | else
209 | @failure_message = body
210 | end
211 | end
212 |
213 | @service_redirect_url = location
214 | end
215 |
216 | def is_success?
217 | !@failure && !ticket.to_s.empty?
218 | end
219 |
220 | def is_failure?
221 | @failure == true
222 | end
223 | end
224 |
225 | class BadResponseException < CASException
226 | end
227 | end
228 |
--------------------------------------------------------------------------------
/lib/casclient/tickets.rb:
--------------------------------------------------------------------------------
1 | module CASClient
2 | # Represents a CAS service ticket.
3 | class ServiceTicket
4 | attr_reader :ticket, :service, :renew
5 | attr_accessor :user, :extra_attributes, :pgt_iou, :success, :failure_code, :failure_message
6 |
7 | def initialize(ticket, service, renew = false)
8 | @ticket = ticket
9 | @service = service
10 | @renew = renew
11 | end
12 |
13 | def is_valid?
14 | success
15 | end
16 |
17 | def has_been_validated?
18 | not user.nil?
19 | end
20 | end
21 |
22 | # Represents a CAS proxy ticket.
23 | class ProxyTicket < ServiceTicket
24 | end
25 |
26 | class ProxyGrantingTicket
27 | attr_reader :ticket, :iou
28 |
29 | def initialize(ticket, iou)
30 | @ticket = ticket
31 | @iou = iou
32 | end
33 |
34 | def to_s
35 | ticket
36 | end
37 | end
38 | end
--------------------------------------------------------------------------------
/lib/casclient/tickets/storage.rb:
--------------------------------------------------------------------------------
1 | module CASClient
2 | module Tickets
3 | module Storage
4 | class AbstractTicketStore
5 |
6 | attr_accessor :log
7 | def log
8 | @log ||= CASClient::LoggerWrapper.new
9 | end
10 |
11 | def process_single_sign_out(st)
12 |
13 | session_id, session = get_session_for_service_ticket(st)
14 | if session
15 | session.destroy
16 | log.debug("Destroyed #{session.inspect} for session #{session_id.inspect} corresponding to service ticket #{st.inspect}.")
17 | else
18 | log.debug("Data for session #{session_id.inspect} was not found. It may have already been cleared by a local CAS logout request.")
19 | end
20 |
21 | if session_id
22 | log.info("Single-sign-out for service ticket #{session_id.inspect} completed successfuly.")
23 | else
24 | log.debug("No session id found for CAS ticket #{st}")
25 | end
26 | end
27 |
28 | def get_session_for_service_ticket(st)
29 | session_id = read_service_session_lookup(st)
30 | unless session_id.nil?
31 | # This feels a bit hackish, but there isn't really a better way to go about it that I am aware of yet
32 | session = ActiveRecord::SessionStore.session_class.find(:first, :conditions => {:session_id => session_id})
33 | else
34 | log.warn("Couldn't destroy session service ticket #{st} because no corresponding session id could be found.")
35 | end
36 | [session_id, session]
37 | end
38 |
39 | def store_service_session_lookup(st, controller)
40 | raise 'Implement this in a subclass!'
41 | end
42 |
43 | def cleanup_service_session_lookup(st)
44 | raise 'Implement this in a subclass!'
45 | end
46 |
47 | def save_pgt_iou(pgt_iou, pgt)
48 | raise 'Implement this in a subclass!'
49 | end
50 |
51 | def retrieve_pgt(pgt_iou)
52 | raise 'Implement this in a subclass!'
53 | end
54 |
55 | protected
56 | def read_service_session_lookup(st)
57 | raise 'Implement this in a subclass!'
58 | end
59 |
60 | def session_id_from_controller(controller)
61 | session_id = controller.request.session_options[:id] || controller.session.session_id
62 | raise CASClient::CASException, "Failed to extract session_id from controller" if session_id.nil?
63 | session_id
64 | end
65 | end
66 |
67 | # A Ticket Store that keeps it's tickets in a directory on the local filesystem.
68 | # Service tickets are stored under tmp/sessions by default
69 | # and Proxy Granting Tickets and their IOUs are stored in tmp/cas_pgt.pstore
70 | # This Ticket Store works fine for small sites but will most likely have
71 | # concurrency problems under heavy load. It also requires that all your
72 | # worker processes have access to a shared file system.
73 | #
74 | # This ticket store takes the following config parameters
75 | # :storage_dir - The directory to store data in. Defaults to Rails.root/tmp
76 | # :service_session_lookup_dir - The directory to store Service Ticket/Session ID files in. Defaults to :storage_dir/sessions
77 | # :pgt_store_path - The location to store the pgt PStore file. Defaults to :storage_dir/cas_pgt.pstore
78 | class LocalDirTicketStore < AbstractTicketStore
79 | require 'pstore'
80 |
81 | def initialize(config={})
82 | config ||= {}
83 | default_tmp_dir = defined?(Rails.root) ? "#{Rails.root}/tmp" : "#{Dir.pwd}/tmp"
84 | @tmp_dir = config[:storage_dir] || default_tmp_dir
85 | @service_session_lookup_dir = config[:service_session_lookup_dir] || "#{@tmp_dir}/sessions"
86 | @pgt_store_path = config[:pgt_store_path] || "#{@tmp_dir}/cas_pgt.pstore"
87 | end
88 |
89 | # Creates a file in tmp/sessions linking a SessionTicket
90 | # with the local Rails session id. The file is named
91 | # cas_sess. and its text contents is the corresponding
92 | # Rails session id.
93 | # Returns the filename of the lookup file created.
94 | def store_service_session_lookup(st, controller)
95 | raise CASException, "No service_ticket specified." if st.nil?
96 | raise CASException, "No controller specified." if controller.nil?
97 |
98 | sid = session_id_from_controller(controller)
99 |
100 | st = st.ticket if st.kind_of? ServiceTicket
101 | f = File.new(filename_of_service_session_lookup(st), 'w')
102 | f.write(sid)
103 | f.close
104 | return f.path
105 | end
106 |
107 | # Returns the local Rails session ID corresponding to the given
108 | # ServiceTicket. This is done by reading the contents of the
109 | # cas_sess. file created in a prior call to
110 | # #store_service_session_lookup.
111 | def read_service_session_lookup(st)
112 | raise CASException, "No service_ticket specified." if st.nil?
113 |
114 | st = st.ticket if st.kind_of? ServiceTicket
115 | ssl_filename = filename_of_service_session_lookup(st)
116 | return IO.read(ssl_filename) if File.exists?(ssl_filename)
117 | end
118 |
119 | # Removes a stored relationship between a ServiceTicket and a local
120 | # Rails session id. This should be called when the session is being
121 | # closed.
122 | #
123 | # See #store_service_session_lookup.
124 | def cleanup_service_session_lookup(st)
125 | raise CASException, "No service_ticket specified." if st.nil?
126 |
127 | st = st.ticket if st.kind_of? ServiceTicket
128 | ssl_filename = filename_of_service_session_lookup(st)
129 | File.delete(ssl_filename) if File.exists?(ssl_filename)
130 | end
131 |
132 | def save_pgt_iou(pgt_iou, pgt)
133 | raise CASException, "Invalid pgt_iou" if pgt_iou.nil?
134 | raise CASException, "Invalid pgt" if pgt.nil?
135 |
136 | # TODO: pstore contents should probably be encrypted...
137 | pstore = open_pstore
138 |
139 | pstore.transaction do
140 | pstore[pgt_iou] = pgt
141 | end
142 | end
143 |
144 | def retrieve_pgt(pgt_iou)
145 | raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
146 |
147 | pstore = open_pstore
148 |
149 | pgt = nil
150 | # TODO: need to periodically clean the storage, otherwise it will just keep growing
151 | pstore.transaction do
152 | pgt = pstore[pgt_iou]
153 | pstore.delete pgt_iou
154 | end
155 |
156 | raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgt
157 |
158 | pgt
159 | end
160 |
161 | private
162 |
163 | # Returns the path and filename of the service session lookup file.
164 | def filename_of_service_session_lookup(st)
165 | st = st.ticket if st.kind_of? ServiceTicket
166 | return "#{@service_session_lookup_dir}/cas_sess.#{st}"
167 | end
168 |
169 | def open_pstore
170 | PStore.new(@pgt_store_path)
171 | end
172 | end
173 | end
174 | end
175 | end
176 |
--------------------------------------------------------------------------------
/lib/casclient/tickets/storage/active_record_ticket_store.rb:
--------------------------------------------------------------------------------
1 | module CASClient
2 | module Tickets
3 | module Storage
4 |
5 | # A Ticket Store that keeps it's ticket in database tables using ActiveRecord.
6 | #
7 | # Services Tickets are stored in an extra column added to the ActiveRecord sessions table.
8 | # You will need to add the service_ticket column your ActiveRecord sessions table.
9 | # Proxy Granting Tickets and their IOUs are stored in the cas_pgtious table.
10 | #
11 | # This ticket store takes the following config parameters
12 | # :pgtious_table_name - the name of the table
13 | class ActiveRecordTicketStore < AbstractTicketStore
14 |
15 | def initialize(config={})
16 | config ||= {}
17 | if config[:pgtious_table_name]
18 | CasPgtiou.set_table_name = config[:pgtious_table_name]
19 | end
20 | ActiveRecord::SessionStore.session_class = ServiceTicketAwareSession
21 | end
22 |
23 | def store_service_session_lookup(st, controller)
24 | raise CASException, "No service_ticket specified." unless st
25 | raise CASException, "No controller specified." unless controller
26 |
27 | st = st.ticket if st.kind_of? ServiceTicket
28 | session = controller.session
29 | session[:service_ticket] = st
30 | end
31 |
32 | def read_service_session_lookup(st)
33 | raise CASException, "No service_ticket specified." unless st
34 | st = st.ticket if st.kind_of? ServiceTicket
35 | session = ActiveRecord::SessionStore::Session.find_by_service_ticket(st)
36 | session ? session.session_id : nil
37 | end
38 |
39 | def cleanup_service_session_lookup(st)
40 | #no cleanup needed for this ticket store
41 | #we still raise the exception for API compliance
42 | raise CASException, "No service_ticket specified." unless st
43 | end
44 |
45 | def save_pgt_iou(pgt_iou, pgt)
46 | raise CASClient::CASException.new("Invalid pgt_iou") if pgt_iou.nil?
47 | raise CASClient::CASException.new("Invalid pgt") if pgt.nil?
48 | pgtiou = CasPgtiou.create(:pgt_iou => pgt_iou, :pgt_id => pgt)
49 | end
50 |
51 | def retrieve_pgt(pgt_iou)
52 | raise CASException, "No pgt_iou specified. Cannot retrieve the pgt." unless pgt_iou
53 |
54 | pgtiou = CasPgtiou.find_by_pgt_iou(pgt_iou)
55 |
56 | raise CASException, "Invalid pgt_iou specified. Perhaps this pgt has already been retrieved?" unless pgtiou
57 | pgt = pgtiou.pgt_id
58 |
59 | pgtiou.destroy
60 |
61 | pgt
62 |
63 | end
64 |
65 | end
66 |
67 | ACTIVE_RECORD_TICKET_STORE = ActiveRecordTicketStore
68 |
69 | class ServiceTicketAwareSession < ActiveRecord::SessionStore::Session
70 | before_save :save_service_ticket
71 |
72 | def save_service_ticket
73 | if data[:service_ticket]
74 | self.service_ticket = data[:service_ticket]
75 | end
76 | end
77 | end
78 |
79 | class CasPgtiou < ActiveRecord::Base
80 | #t.string :pgt_iou, :null => false
81 | #t.string :pgt_id, :null => false
82 | #t.timestamps
83 | end
84 | end
85 | end
86 | end
87 |
--------------------------------------------------------------------------------
/lib/rubycas-client.rb:
--------------------------------------------------------------------------------
1 | require 'casclient'
2 |
--------------------------------------------------------------------------------
/lib/rubycas-client/version.rb:
--------------------------------------------------------------------------------
1 | module CasClient
2 | VERSION = "2.3.10.rc1"
3 | end
4 |
--------------------------------------------------------------------------------
/rails_generators/active_record_ticket_store/USAGE:
--------------------------------------------------------------------------------
1 | Description:
2 | Create a migration to add the service_ticket column to the sessions
3 | table and create the cas_pgtious table for proxy ticket storage.
4 | Pass the migration name as an optional parameter. The migration name
5 | defaults to CreateActiveRecordTicketStore.
6 |
7 | Requirements:
8 | You need to already have created the ActiveRecord::SessionStore sessions
9 | table.
10 |
--------------------------------------------------------------------------------
/rails_generators/active_record_ticket_store/active_record_ticket_store_generator.rb:
--------------------------------------------------------------------------------
1 | class ActiveRecordTicketStoreGenerator < Rails::Generator::NamedBase
2 |
3 | def initialize(runtime_args, runtime_options = {})
4 | runtime_args << 'create_active_record_ticket_store' if runtime_args.empty?
5 | super
6 | end
7 |
8 | def manifest
9 | record do |m|
10 | m.migration_template 'migration.rb', 'db/migrate',
11 | :assigns => { :session_table_name => default_session_table_name, :pgtiou_table_name => default_pgtiou_table_name }
12 | m.readme "README"
13 | end
14 | end
15 |
16 | protected
17 | def banner
18 | "Usage: #{$0} #{spec.name} [CreateActiveRecordTicketStore] [options]"
19 | end
20 |
21 | def default_session_table_name
22 | ActiveRecord::Base.pluralize_table_names ? 'session'.pluralize : 'session'
23 | end
24 |
25 | def default_pgtiou_table_name
26 | ActiveRecord::Base.pluralize_table_names ? 'cas_pgtiou'.pluralize : 'cas_pgtiou'
27 | end
28 |
29 | end
30 |
--------------------------------------------------------------------------------
/rails_generators/active_record_ticket_store/templates/README:
--------------------------------------------------------------------------------
1 | You need to make sure you have already created the sessions table for the ActiveRecord::SessionStore
2 |
--------------------------------------------------------------------------------
/rails_generators/active_record_ticket_store/templates/migration.rb:
--------------------------------------------------------------------------------
1 | class <%= class_name %> < ActiveRecord::Migration
2 | def self.up
3 | add_column :<%= session_table_name %>, :service_ticket, :string
4 |
5 | add_index :<%= session_table_name %>, :service_ticket
6 |
7 | create_table :<%= pgtiou_table_name %> do |t|
8 | t.string :pgt_iou, :null => false
9 | t.string :pgt_id, :null => false
10 | t.timestamps
11 | end
12 |
13 | add_index :<%= pgtiou_table_name %>, :pgt_iou, :unique => true
14 | end
15 |
16 | def self.down
17 | drop_table :<%= pgtiou_table_name %>
18 |
19 | remove_index :<%= session_table_name %>, :service_ticket
20 |
21 | remove_column :<%= session_table_name %>, :service_ticket
22 | end
23 | end
24 |
25 |
--------------------------------------------------------------------------------
/rubycas-client.gemspec:
--------------------------------------------------------------------------------
1 | # -*- encoding: utf-8 -*-
2 | $LOAD_PATH << File.expand_path("../lib", __FILE__)
3 | require 'rubycas-client/version'
4 |
5 | Gem::Specification.new do |gem|
6 | gem.authors = ["Matt Campbell", "Matt Zukowski", "Matt Walker", "Matt Campbell"]
7 | gem.email = ["matt@soupmatt.com"]
8 | gem.summary = %q{Client library for the Central Authentication Service (CAS) protocol.}
9 | gem.description = %q{Client library for the Central Authentication Service (CAS) protocol.}
10 | gem.homepage = "https://github.com/rubycas/rubycas-client"
11 | gem.extra_rdoc_files = [
12 | "LICENSE.txt",
13 | "README.rdoc"
14 | ]
15 | gem.licenses = ["MIT"]
16 | gem.rdoc_options = ["--main", "README.rdoc"]
17 | gem.version = CasClient::VERSION
18 |
19 | gem.files = `git ls-files`.split($\)
20 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
21 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
22 | gem.name = "rubycas-client"
23 | gem.require_paths = ["lib"]
24 |
25 | gem.add_dependency("activesupport")
26 | gem.add_development_dependency("rake")
27 | gem.add_development_dependency("database_cleaner", "~> 0.9.1")
28 | gem.add_development_dependency("json")
29 | gem.add_development_dependency("rspec")
30 | gem.add_development_dependency("appraisal")
31 | gem.add_development_dependency("rails")
32 | gem.add_development_dependency("simplecov")
33 | if defined?(JRUBY_VERSION)
34 | gem.add_development_dependency("activerecord-jdbcsqlite3-adapter")
35 | else
36 | gem.add_development_dependency("sqlite3")
37 | end
38 | end
39 |
--------------------------------------------------------------------------------
/spec/.gitignore:
--------------------------------------------------------------------------------
1 | test.*.db
2 |
--------------------------------------------------------------------------------
/spec/casclient/client_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe CASClient::Client do
4 | let(:client) { CASClient::Client.new(:login_url => login_url, :cas_base_url => '')}
5 | let(:login_url) { "http://localhost:3443/"}
6 | let(:uri) { URI.parse(login_url) }
7 | let(:session) { double('session', :use_ssl= => true, :verify_mode= => true) }
8 |
9 | context "https connection" do
10 | let(:proxy) { double('proxy', :new => session) }
11 |
12 | before :each do
13 | Net::HTTP.stub :Proxy => proxy
14 | end
15 |
16 | it "sets up the session with the login url host and port" do
17 | proxy.should_receive(:new).with('localhost', 3443).and_return(session)
18 | client.send(:https_connection, uri)
19 | end
20 |
21 | it "sets up the proxy with the known proxy host and port" do
22 | client = CASClient::Client.new(:login_url => login_url, :cas_base_url => '', :proxy_host => 'foo', :proxy_port => 1234)
23 | Net::HTTP.should_receive(:Proxy).with('foo', 1234).and_return(proxy)
24 | client.send(:https_connection, uri)
25 | end
26 | end
27 |
28 | context "cas server requests" do
29 | let(:response) { double('response', :body => 'HTTP BODY', :code => '200') }
30 | let(:connection) { double('connection', :get => response, :post => response, :request => response) }
31 |
32 | before :each do
33 | client.stub(:https_connection).and_return(session)
34 | session.stub(:start).and_yield(connection)
35 | end
36 |
37 | context "cas server is up" do
38 | it "returns false if the server cannot be connected to" do
39 | connection.stub(:get).and_raise(Errno::ECONNREFUSED)
40 | client.cas_server_is_up?.should be_false
41 | end
42 |
43 | it "returns false if the request was not a success" do
44 | response.stub :kind_of? => false
45 | client.cas_server_is_up?.should be_false
46 | end
47 |
48 | it "returns true when the server is running" do
49 | response.stub :kind_of? => true
50 | client.cas_server_is_up?.should be_true
51 | end
52 | end
53 |
54 | context "request login ticket" do
55 | it "raises an exception when the request was not a success" do
56 | session.stub(:post).with("/Ticket", ";").and_return(response)
57 | response.stub :kind_of? => false
58 | lambda {
59 | client.request_login_ticket
60 | }.should raise_error(CASClient::CASException)
61 | end
62 |
63 | it "returns the response body when the request is a success" do
64 | session.stub(:post).with("/Ticket", ";").and_return(response)
65 | response.stub :kind_of? => true
66 | client.request_login_ticket.should == "HTTP BODY"
67 | end
68 | end
69 |
70 | context "request cas response" do
71 | let(:validation_response) { double('validation_response') }
72 |
73 | it "should raise an exception when the request is not a success or 422" do
74 | response.stub :kind_of? => false
75 | lambda {
76 | client.send(:request_cas_response, uri, CASClient::ValidationResponse)
77 | }.should raise_error(RuntimeError)
78 | end
79 |
80 | it "should return a ValidationResponse object when the request is a success or 422" do
81 | CASClient::ValidationResponse.stub(:new).and_return(validation_response)
82 | response.stub :kind_of? => true
83 | client.send(:request_cas_response, uri, CASClient::ValidationResponse).should == validation_response
84 | end
85 | end
86 |
87 | context "submit data to cas" do
88 | it "should return an HTTPResponse" do
89 | client.send(:submit_data_to_cas, uri, {}).should == response
90 | end
91 | end
92 | end
93 | end
94 |
--------------------------------------------------------------------------------
/spec/casclient/frameworks/rails/filter_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'action_controller'
3 | require 'casclient/frameworks/rails/filter'
4 |
5 | describe CASClient::Frameworks::Rails::Filter do
6 |
7 | before(:each) do
8 | CASClient::Frameworks::Rails::Filter.configure(
9 | :cas_base_url => 'http://test.local/',
10 | :logger => double("Logger")
11 | )
12 | end
13 |
14 | describe "#fake" do
15 | subject { Hash.new }
16 | context "faking user without attributes" do
17 | before { CASClient::Frameworks::Rails::Filter.fake('tester@test.com') }
18 | it 'should set the session user' do
19 | CASClient::Frameworks::Rails::Filter.filter(mock_controller_with_session(nil, subject))
20 | subject.should eq({:cas_user => 'tester@test.com', :casfilteruser => 'tester@test.com'})
21 | end
22 | after { CASClient::Frameworks::Rails::Filter.fake(nil,nil) }
23 | end
24 |
25 | context "faking user with attributes" do
26 | before { CASClient::Frameworks::Rails::Filter.fake('tester@test.com', {:test => 'stuff', :this => 'that'}) }
27 | it 'should set the session user and attributes' do
28 | CASClient::Frameworks::Rails::Filter.filter(mock_controller_with_session(nil, subject))
29 | subject.should eq({ :cas_user => 'tester@test.com', :casfilteruser => 'tester@test.com', :cas_extra_attributes => {:test => 'stuff', :this => 'that' }})
30 | end
31 | after { CASClient::Frameworks::Rails::Filter.fake(nil,nil) }
32 | end
33 | end
34 |
35 | context "new valid service ticket" do
36 | it "should return successfully from filter" do
37 |
38 | pgt = CASClient::ProxyGrantingTicket.new(
39 | "PGT-1308586001r9573FAD5A8C62E134A4AA93273F226BD3F0C3A983DCCCD176",
40 | "PGTIOU-1308586001r29DC1F852C95930FE6694C1EFC64232A3359798893BC0B")
41 |
42 | raw_text = "
43 |
44 | rich.yarger@vibes.com
45 | PGTIOU-1308586001r29DC1F852C95930FE6694C1EFC64232A3359798893BC0B
46 |
47 | "
48 | response = CASClient::ValidationResponse.new(raw_text)
49 |
50 | CASClient::Client.any_instance.stub(:request_cas_response).and_return(response)
51 | CASClient::Client.any_instance.stub(:retrieve_proxy_granting_ticket).and_return(pgt)
52 |
53 | controller = mock_controller_with_session()
54 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(true)
55 | end
56 | end
57 |
58 | context "new invalid service ticket" do
59 | it "should return failure from filter" do
60 |
61 | raw_text = "
62 | Some Error Text
63 | "
64 | response = CASClient::ValidationResponse.new(raw_text)
65 |
66 | CASClient::Client.any_instance.stub(:request_cas_response).and_return(response)
67 | CASClient::Frameworks::Rails::Filter.stub(:unauthorized!) {"bogusresponse"}
68 |
69 | controller = mock_controller_with_session()
70 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(false)
71 | end
72 | end
73 |
74 | context "does not have new input service ticket" do
75 | context "with last service ticket" do
76 | it "should return failure from filter" do
77 |
78 | CASClient::Frameworks::Rails::Filter.stub(:unauthorized!) {"bogusresponse"}
79 |
80 | controller = mock_controller_with_session()
81 | controller.stub(:params) {{}}
82 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(false)
83 | end
84 | end
85 |
86 | context "sent through gateway" do
87 | context "gatewaying off" do
88 | it "should return failure from filter" do
89 |
90 | CASClient::Frameworks::Rails::Filter.stub(:unauthorized!) {"bogusresponse"}
91 |
92 | CASClient::Frameworks::Rails::Filter.config[:use_gatewaying] = false
93 | controller = mock_controller_with_session()
94 | controller.session[:cas_sent_to_gateway] = true
95 | controller.stub(:params) {{}}
96 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(false)
97 | end
98 | end
99 |
100 | context "gatewaying on" do
101 | it "should return failure from filter" do
102 |
103 | CASClient::Frameworks::Rails::Filter.config[:use_gatewaying] = true
104 | controller = mock_controller_with_session()
105 | controller.session[:cas_sent_to_gateway] = true
106 | controller.stub(:params) {{}}
107 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(true)
108 | end
109 | end
110 | end
111 | end
112 |
113 | context "has new input service ticket" do
114 | context "no PGT" do
115 | it "should return failure from filter" do
116 |
117 | raw_text = "
118 |
119 | rich.yarger@vibes.com
120 | PGTIOU-1308586001r29DC1F852C95930FE6694C1EFC64232A3359798893BC0B
121 |
122 | "
123 | response = CASClient::ValidationResponse.new(raw_text)
124 |
125 | CASClient::Client.any_instance.stub(:request_cas_response).and_return(response)
126 | CASClient::Client.any_instance.stub(:retrieve_proxy_granting_ticket).and_raise CASClient::CASException
127 |
128 | controller = mock_controller_with_session()
129 | expect { CASClient::Frameworks::Rails::Filter.filter(controller) }.to raise_error(CASClient::CASException)
130 | end
131 | end
132 |
133 | context "cannot connect to CASServer" do
134 | it "should return failure from filter" do
135 |
136 | CASClient::Client.any_instance.stub(:request_cas_response).and_raise "Some exception"
137 |
138 | controller = mock_controller_with_session()
139 | expect { CASClient::Frameworks::Rails::Filter.filter(controller) }.to raise_error(RuntimeError)
140 | end
141 | end
142 |
143 | context "matches existing service ticket" do
144 | subject { Hash.new }
145 | it "should return successfully from filter" do
146 |
147 | mock_client = CASClient::Client.new()
148 | mock_client.should_receive(:request_cas_response).at_most(0).times
149 | mock_client.should_receive(:retrieve_proxy_granting_ticket).at_most(0).times
150 | CASClient::Frameworks::Rails::Filter.send(:class_variable_set, :@@client, mock_client)
151 |
152 | subject[:cas_last_valid_ticket] = 'bogusticket'
153 | subject[:cas_last_valid_ticket_service] = 'bogusurl'
154 | controller = mock_controller_with_session(mock_post_request(), subject)
155 | CASClient::Frameworks::Rails::Filter.filter(controller).should eq(true)
156 | end
157 | end
158 | end
159 |
160 | context "controller request is missing format" do
161 | context "#unauthorized!" do
162 | it 'should not crash' do
163 | request = double('mock request')
164 | request.stub(:format).and_return(nil)
165 |
166 | controller = mock_controller_with_session(request)
167 |
168 | CASClient::Frameworks::Rails::Filter.
169 | should_receive(:redirect_to_cas_for_authentication).
170 | with(controller)
171 |
172 | CASClient::Frameworks::Rails::Filter.unauthorized!(controller)
173 | end
174 | end
175 | end
176 | end
177 |
--------------------------------------------------------------------------------
/spec/casclient/tickets/storage/active_record_ticket_store_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'casclient/tickets/storage/active_record_ticket_store'
3 |
4 | describe CASClient::Tickets::Storage::ActiveRecordTicketStore do
5 | it_should_behave_like "a ticket store"
6 | end
7 |
--------------------------------------------------------------------------------
/spec/casclient/tickets/storage_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'support/local_hash_ticket_store'
3 | require 'fileutils'
4 |
5 | describe CASClient::Tickets::Storage::AbstractTicketStore do
6 | describe "#store_service_session_lookup" do
7 | it "should raise an exception" do
8 | expect { subject.store_service_session_lookup("service_ticket", mock_controller_with_session) }.to raise_exception 'Implement this in a subclass!'
9 | end
10 | end
11 | describe "#cleanup_service_session_lookup" do
12 | it "should raise an exception" do
13 | expect { subject.cleanup_service_session_lookup("service_ticket") }.to raise_exception 'Implement this in a subclass!'
14 | end
15 | end
16 | describe "#save_pgt_iou" do
17 | it "should raise an exception" do
18 | expect { subject.save_pgt_iou("pgt_iou", "pgt") }.to raise_exception 'Implement this in a subclass!'
19 | end
20 | end
21 | describe "#retrieve_pgt" do
22 | it "should raise an exception" do
23 | expect { subject.retrieve_pgt("pgt_iou") }.to raise_exception 'Implement this in a subclass!'
24 | end
25 | end
26 | describe "#get_session_for_service_ticket" do
27 | it "should raise an exception" do
28 | expect { subject.get_session_for_service_ticket("service_ticket") }.to raise_exception 'Implement this in a subclass!'
29 | end
30 | end
31 | end
32 |
33 | describe CASClient::Tickets::Storage::LocalDirTicketStore do
34 | let(:dir) {File.join(SPEC_TMP_DIR, "local_dir_ticket_store")}
35 | before do
36 | FileUtils.mkdir_p(File.join(dir, "sessions"))
37 | end
38 | after do
39 | FileUtils.remove_dir(dir)
40 | end
41 | it_should_behave_like "a ticket store" do
42 | let(:ticket_store) {described_class.new(:storage_dir => dir)}
43 | end
44 | end
45 |
--------------------------------------------------------------------------------
/spec/casclient/validation_response_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 | require 'casclient/responses.rb'
3 |
4 | describe CASClient::ValidationResponse do
5 | context "when parsing extra attributes as raw" do
6 | let(:response_text) do
7 | <
9 |
10 |
11 | Jimmy Bob
12 |
14 |
16 |
17 |
18 |
19 |
20 | RESPONSE_TEXT
21 | end
22 |
23 | subject { CASClient::ValidationResponse.new response_text, :encode_extra_attributes_as => :raw }
24 |
25 | it "sets text attributes to their string value" do
26 | subject.extra_attributes["name"].should == "Jimmy Bob"
27 | end
28 |
29 | it "preserves whitespace for CDATA" do
30 | subject.extra_attributes["status"].should == "stuff\n"
31 | end
32 |
33 | it "passes yaml through as is" do
34 | subject.extra_attributes["yaml"].should == "--- true\n"
35 | end
36 | it "passes json through as is" do
37 | subject.extra_attributes["json"].should == "{\"id\":10529}"
38 | end
39 | end
40 |
41 | context "when parsing extra attributes as yaml" do
42 | let(:response_text) do
43 | <
45 |
46 |
47 | Jimmy Bob
48 |
50 |
52 |
53 |
54 |
55 |
56 | RESPONSE_TEXT
57 | end
58 |
59 | subject { CASClient::ValidationResponse.new response_text, :encode_extra_attributes_as => :yaml }
60 |
61 | it "sets text attributes to their string value" do
62 | subject.extra_attributes["name"].should == "Jimmy Bob"
63 | end
64 |
65 | it "sets the value of boolean attributes to their boolean value" do
66 | subject.extra_attributes["falsy"].should == false
67 | subject.extra_attributes["truthy"].should == true
68 | end
69 | end
70 |
71 | context "when parsing extra attributes as JSON" do
72 | let(:response_text) do
73 | <
75 |
76 |
77 | Jack
78 | 92.5
79 |
80 |
81 |
82 |
83 | - 10
84 |
86 |
88 |
89 |
90 |
91 | RESPONSE_TEXT
92 | end
93 |
94 | subject { CASClient::ValidationResponse.new response_text, :encode_extra_attributes_as => :json }
95 |
96 | it "sets the value of non-CDATA escaped empty attribute to nil" do
97 | subject.extra_attributes["mobile_phone"].should be_nil
98 | end
99 |
100 | it "sets the value of CDATA escaped empty attribute to nil" do
101 | subject.extra_attributes["global_roles"].should be_nil
102 | end
103 |
104 | it "sets the value of literal attributes to their value" do
105 | subject.extra_attributes["first_name"].should == "Jack"
106 | end
107 |
108 | it "sets the value of JSON attributes containing Arrays to their parsed value" do
109 | subject.extra_attributes["foo_data"][0]["id"].should == 10529
110 | end
111 |
112 | it "sets the value of JSON attributes containing Hashes to their parsed value" do
113 | subject.extra_attributes["food_data"]["id"].should == 10529
114 | end
115 |
116 | it "sets non-hash attributes as strings" do
117 | subject.extra_attributes["last_name"].should be_a_kind_of String
118 | end
119 |
120 | it "sets the value of attributes which are not valid JSON but are valid YAML to their literal value" do
121 | subject.extra_attributes["allegedly_yaml"].should == '- 10'
122 | end
123 | end
124 |
125 | context "When parsing extra attributes from xml attributes" do
126 | let(:response_text) do
127 | <
129 |
130 |
131 | myuser
132 |
133 |
134 |
135 |
136 |
137 | RESPONSE_TEXT
138 | end
139 |
140 | subject { CASClient::ValidationResponse.new response_text }
141 |
142 | it "sets attributes for other type of format" do
143 | expected = {"username" => "myuser", "name" => 'My User', "email" => 'myuser@mail.example.com'}
144 | subject.user.should == 'myuser'
145 | subject.extra_attributes.should == expected
146 | end
147 | end
148 | end
149 |
--------------------------------------------------------------------------------
/spec/database.yml:
--------------------------------------------------------------------------------
1 | test:
2 | adapter: sqlite3
3 | database: spec/test.sqlite3.db
4 |
5 | testjruby:
6 | adapter: jdbcsqlite3
7 | database: spec/test.sqlite3.db
8 |
--------------------------------------------------------------------------------
/spec/spec_helper.rb:
--------------------------------------------------------------------------------
1 | require 'bundler'
2 | Bundler.setup(:default, :development)
3 | require 'simplecov' unless ENV['TRAVIS'] || defined?(JRUBY_VERSION)
4 | Bundler.require
5 |
6 | require 'rubycas-client'
7 |
8 | SPEC_TMP_DIR="spec/tmp"
9 |
10 | Dir["./spec/support/**/*.rb"].each do |f|
11 | require f.gsub('.rb','') unless f.end_with? '_spec.rb'
12 | end
13 |
14 | require 'database_cleaner'
15 |
16 | RSpec.configure do |config|
17 | config.mock_with :rspec
18 | config.mock_framework = :rspec
19 | config.include ActionControllerHelpers
20 |
21 | config.treat_symbols_as_metadata_keys_with_true_values = true
22 | config.filter_run_including :focus
23 | config.run_all_when_everything_filtered = true
24 | config.fail_fast = false
25 |
26 | config.before(:suite) do
27 | ActiveRecordHelpers.setup_active_record
28 | DatabaseCleaner.strategy = :transaction
29 | DatabaseCleaner.clean_with(:truncation)
30 | end
31 |
32 | config.after(:suite) do
33 | ActiveRecordHelpers.teardown_active_record
34 | end
35 |
36 | config.before(:each) do
37 | DatabaseCleaner.start
38 | end
39 |
40 | config.after(:each) do
41 | DatabaseCleaner.clean
42 | end
43 | end
44 |
45 |
--------------------------------------------------------------------------------
/spec/support/action_controller_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'action_pack'
2 |
3 | module ActionControllerHelpers
4 |
5 | def mock_controller_with_session(request = nil, session={})
6 |
7 | query_parameters = {:ticket => "bogusticket", :renew => false}
8 | parameters = query_parameters.dup
9 |
10 | #TODO this really need to be replaced with a "real" rails controller
11 | request ||= mock_post_request
12 | request.stub(:query_parameters) {query_parameters}
13 | request.stub(:path_parameters) {{}}
14 | controller = double("Controller")
15 | controller.stub(:session) {session}
16 | controller.stub(:request) {request}
17 | controller.stub(:url_for) {"bogusurl"}
18 | controller.stub(:query_parameters) {query_parameters}
19 | controller.stub(:path_parameters) {{}}
20 | controller.stub(:parameters) {parameters}
21 | controller.stub(:params) {parameters}
22 | controller
23 | end
24 |
25 | def mock_post_request
26 | mock_request = double("request")
27 | mock_request.stub(:post?) {true}
28 | mock_request.stub(:session_options) { Hash.new }
29 | mock_request.stub(:headers) { Hash.new }
30 | mock_request
31 | end
32 | end
33 |
--------------------------------------------------------------------------------
/spec/support/active_record_helpers.rb:
--------------------------------------------------------------------------------
1 | require 'active_record'
2 |
3 | module ActiveRecordHelpers
4 |
5 | class << self
6 | def setup_active_record
7 | config_file = File.open("spec/database.yml")
8 | db_config = HashWithIndifferentAccess.new(YAML.load(config_file))
9 | ActiveRecord::Base.establish_connection(db_config[(RUBY_PLATFORM == "java") ? :testjruby : :test])
10 | ActiveRecord::Migration.verbose = false
11 | RubyCasTables.migrate(:up)
12 | end
13 |
14 | def teardown_active_record
15 | ActiveRecord::Migration.verbose = false
16 | RubyCasTables.migrate(:down)
17 | end
18 | end
19 |
20 | class RubyCasTables < ActiveRecord::Migration
21 | def self.up
22 | #default rails sessions table
23 | create_table :sessions do |t|
24 | t.string :session_id, :null => false
25 | t.text :data
26 | t.timestamps
27 | end
28 | add_index :sessions, :session_id
29 | add_index :sessions, :updated_at
30 |
31 | #column added to sessions table by rubycas-client
32 | add_column :sessions, :service_ticket, :string
33 | add_index :sessions, :service_ticket
34 |
35 | # pgtious table
36 | create_table :cas_pgtious do |t|
37 | t.string :pgt_iou, :null => false
38 | t.string :pgt_id, :null => false
39 | t.timestamps
40 | end
41 | end
42 |
43 | def self.down
44 | drop_table :sessions
45 | drop_table :cas_pgtious
46 | end
47 | end
48 | end
49 |
--------------------------------------------------------------------------------
/spec/support/local_hash_ticket_store.rb:
--------------------------------------------------------------------------------
1 | require 'casclient/tickets/storage'
2 |
3 | class LocalHashTicketStore < CASClient::Tickets::Storage::AbstractTicketStore
4 |
5 | attr_accessor :st_hash
6 | attr_accessor :pgt_hash
7 |
8 | def store_service_session_lookup(st, controller)
9 | raise CASClient::CASException, "No service_ticket specified." if st.nil?
10 | raise CASClient::CASException, "No controller specified." if controller.nil?
11 | session_id = session_id_from_controller(controller)
12 | st = st.ticket if st.kind_of? CASClient::ServiceTicket
13 | st_hash[st] = session_id
14 | end
15 |
16 | def read_service_session_lookup(st)
17 | raise CASClient::CASException, "No service_ticket specified." if st.nil?
18 | st = st.ticket if st.kind_of? CASClient::ServiceTicket
19 | st_hash[st]
20 | end
21 |
22 | def cleanup_service_session_lookup(st)
23 | raise CASClient::CASException, "No service_ticket specified." if st.nil?
24 | st = st.ticket if st.kind_of? CASClient::ServiceTicket
25 | st_hash.delete(st)
26 | end
27 |
28 | def save_pgt_iou(pgt_iou, pgt)
29 | raise CASClient::CASException.new("Invalid pgt_iou") if pgt_iou.nil?
30 | raise CASClient::CASException.new("Invalid pgt") if pgt.nil?
31 | pgt_hash[pgt_iou] = pgt
32 | end
33 |
34 | def retrieve_pgt(pgt_iou)
35 | pgt = pgt_hash.delete(pgt_iou)
36 | raise CASClient::CASException.new("Invalid pgt_iou") if pgt.nil?
37 | pgt
38 | end
39 |
40 | def pgt_hash
41 | @pgt_hash ||= {}
42 | end
43 |
44 | def st_hash
45 | @pgt_hash ||= {}
46 | end
47 |
48 | end
49 |
--------------------------------------------------------------------------------
/spec/support/local_hash_ticket_store_spec.rb:
--------------------------------------------------------------------------------
1 | require 'spec_helper'
2 |
3 | describe LocalHashTicketStore do
4 | it_should_behave_like "a ticket store"
5 | end
6 |
--------------------------------------------------------------------------------
/spec/support/shared_examples_for_ticket_stores.rb:
--------------------------------------------------------------------------------
1 | shared_examples "a ticket store interacting with sessions" do
2 | describe "#store_service_session_lookup" do
3 | it "should raise CASException if the Service Ticket is nil" do
4 | expect { subject.store_service_session_lookup(nil, "controller") }.to raise_exception(CASClient::CASException, /No service_ticket specified/)
5 | end
6 | it "should raise CASException if the controller is nil" do
7 | expect { subject.store_service_session_lookup("service_ticket", nil) }.to raise_exception(CASClient::CASException, /No controller specified/)
8 | end
9 | it "should store the ticket without any errors" do
10 | expect { subject.store_service_session_lookup(service_ticket, mock_controller_with_session(nil, session)) }.to_not raise_exception
11 | end
12 | end
13 |
14 | describe "#get_session_for_service_ticket" do
15 | context "the service ticket is nil" do
16 | it "should raise CASException" do
17 | expect { subject.get_session_for_service_ticket(nil) }.to raise_exception(CASClient::CASException, /No service_ticket specified/)
18 | end
19 | end
20 | context "the service ticket is associated with a session" do
21 | before do
22 | subject.store_service_session_lookup(service_ticket, mock_controller_with_session(nil, session))
23 | session.save!
24 | end
25 | it "should return the session_id and session for the given service ticket" do
26 | result_session_id, result_session = subject.get_session_for_service_ticket(service_ticket)
27 | result_session_id.should == session.session_id
28 | result_session.session_id.should == session.session_id
29 | result_session.data.should == session.data
30 | end
31 | end
32 | context "the service ticket is not associated with a session" do
33 | it "should return nils if there is no session for the given service ticket" do
34 | subject.get_session_for_service_ticket(service_ticket).should == [nil, nil]
35 | end
36 | end
37 | end
38 |
39 | describe "#process_single_sign_out" do
40 | context "the service ticket is nil" do
41 | it "should raise CASException" do
42 | expect { subject.process_single_sign_out(nil) }.to raise_exception(CASClient::CASException, /No service_ticket specified/)
43 | end
44 | end
45 | context "the service ticket is associated with a session" do
46 | before do
47 | subject.store_service_session_lookup(service_ticket, mock_controller_with_session(nil, session))
48 | session.save!
49 | subject.process_single_sign_out(service_ticket)
50 | end
51 | context "the session" do
52 | it "should be destroyed" do
53 | ActiveRecord::SessionStore.session_class.find(:first, :conditions => {:session_id => session.session_id}).should be_nil
54 | end
55 | end
56 | it "should destroy session for the given service ticket" do
57 | subject.process_single_sign_out(service_ticket)
58 | end
59 | end
60 | context "the service ticket is not associated with a session" do
61 | it "should run without error if there is no session for the given service ticket" do
62 | expect { subject.process_single_sign_out(service_ticket) }.to_not raise_error
63 | end
64 | end
65 | end
66 |
67 | describe "#cleanup_service_session_lookup" do
68 | context "the service ticket is nil" do
69 | it "should raise CASException" do
70 | expect { subject.cleanup_service_session_lookup(nil) }.to raise_exception(CASClient::CASException, /No service_ticket specified/)
71 | end
72 | end
73 | it "should run without error" do
74 | expect { subject.cleanup_service_session_lookup(service_ticket) }.to_not raise_exception
75 | end
76 | end
77 | end
78 |
79 | shared_examples "a ticket store" do
80 | let(:ticket_store) { described_class.new }
81 | let(:service_url) { "https://www.example.com/cas" }
82 | let(:session) do
83 | ActiveRecord::SessionStore::Session.create!(:session_id => "session#{rand(1000)}", :data => {})
84 | end
85 | subject { ticket_store }
86 |
87 | context "when dealing with sessions, Service Tickets, and Single Sign Out" do
88 | context "and the service ticket is a String" do
89 | it_behaves_like "a ticket store interacting with sessions" do
90 | let(:service_ticket) { "ST-ABC#{rand(1000)}" }
91 | end
92 | end
93 | context "and the service ticket is a ServiceTicket" do
94 | it_behaves_like "a ticket store interacting with sessions" do
95 | let(:service_ticket) { CASClient::ServiceTicket.new("ST-ABC#{rand(1000)}", service_url) }
96 | end
97 | end
98 | context "and the service ticket is a ProxyTicket" do
99 | it_behaves_like "a ticket store interacting with sessions" do
100 | let(:service_ticket) { CASClient::ProxyTicket.new("ST-ABC#{rand(1000)}", service_url) }
101 | end
102 | end
103 | end
104 |
105 | context "when dealing with Proxy Granting Tickets and their IOUs" do
106 | let(:pgt) { "my_pgt_#{rand(1000)}" }
107 | let(:pgt_iou) { "my_pgt_iou_#{rand(1000)}" }
108 |
109 | describe "#save_pgt_iou" do
110 | it "should raise CASClient::CASException if the pgt_iou is nil" do
111 | expect { subject.save_pgt_iou(nil, pgt) }.to raise_exception(CASClient::CASException, /Invalid pgt_iou/)
112 | end
113 | it "should raise CASClient::CASException if the pgt is nil" do
114 | expect { subject.save_pgt_iou(pgt_iou, nil) }.to raise_exception(CASClient::CASException, /Invalid pgt/)
115 | end
116 | end
117 |
118 | describe "#retrieve_pgt" do
119 | before do
120 | subject.save_pgt_iou(pgt_iou, pgt)
121 | end
122 |
123 | it "should return the stored pgt" do
124 | subject.retrieve_pgt(pgt_iou).should == pgt
125 | end
126 |
127 | it "should raise CASClient::CASException if the pgt_iou isn't in the store" do
128 | expect { subject.retrieve_pgt("not_my"+pgt_iou) }.to raise_exception(CASClient::CASException, /Invalid pgt_iou/)
129 | end
130 |
131 | it "should not return the stored pgt a second time" do
132 | subject.retrieve_pgt(pgt_iou).should == pgt
133 | expect { subject.retrieve_pgt(pgt_iou) }.to raise_exception(CASClient::CASException, /Invalid pgt_iou/)
134 | end
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------