From 4c53cd65d272466e1a400db148dbf17020cea730 Mon Sep 17 00:00:00 2001 From: akki-ng Date: Mon, 17 Apr 2017 21:34:05 +0530 Subject: [PATCH 1/2] Multi valued attribute support --- lib/casclient/responses.rb | 41 +++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/lib/casclient/responses.rb b/lib/casclient/responses.rb index 89fdd579..e5f5149c 100644 --- a/lib/casclient/responses.rb +++ b/lib/casclient/responses.rb @@ -7,12 +7,12 @@ def check_and_parse_xml(raw_xml) begin doc = REXML::Document.new(raw_xml, :raw => :all) rescue REXML::ParseException => e - raise BadResponseException, + raise BadResponseException, "MALFORMED CAS RESPONSE:\n#{raw_xml.inspect}\n\nEXCEPTION:\n#{e}" end unless doc.elements && doc.elements["cas:serviceResponse"] - raise BadResponseException, + raise BadResponseException, "This does not appear to be a valid CAS response (missing cas:serviceResponse root element)!\nXML DOC:\n#{doc.to_s}" end @@ -35,8 +35,23 @@ def initialize(raw_text, options={}) parse(raw_text, options) end + def merge_proper(object, key, value) + if object.key?key + if object[key].kind_of?(Array) + arrayVal = object[key] + else + arrayVal = Array.new + arrayVal << object[key] + object[key] = arrayVal + end + arrayVal << value + else + object[key] = value + end + end + def parse(raw_text, options) - raise BadResponseException, + raise BadResponseException, "CAS response is empty/blank." if raw_text.to_s.empty? @parse_datetime = Time.now if raw_text =~ /^(yes|no)\n(.*?)\n$/m @@ -51,6 +66,7 @@ def parse(raw_text, options) # if we got this far then we've got a valid XML response, so we're doing CAS 2.0 @protocol = 2.0 + if is_success? cas_user = @xml.elements["cas:user"] @user = cas_user.text.strip if cas_user @@ -72,7 +88,7 @@ def parse(raw_text, options) name = attrs['name'] inner_text = attrs['value'] end - @extra_attributes.merge! name => inner_text + merge_proper(@extra_attributes, name, inner_text) end # unserialize extra attributes @@ -89,6 +105,7 @@ def parse(raw_text, options) end def parse_extra_attribute_value(value, encode_extra_attributes_as) + value = value.to_s attr_value = if value.to_s.empty? nil elsif !encode_extra_attributes_as @@ -127,7 +144,7 @@ def is_failure? end end - # Represents a response from the CAS server to a proxy ticket request + # Represents a response from the CAS server to a proxy ticket request # (i.e. after requesting a proxy ticket). class ProxyResponse include XmlResponse @@ -139,7 +156,7 @@ def initialize(raw_text, options={}) end def parse(raw_text) - raise BadResponseException, + raise BadResponseException, "CAS response is empty/blank." if raw_text.to_s.empty? @parse_datetime = Time.now @@ -166,7 +183,7 @@ def is_failure? end end - # Represents a response from the CAS server to a login request + # Represents a response from the CAS server to a login request # (i.e. after submitting a username/password). class LoginResponse attr_reader :tgt, :ticket, :service_redirect_url @@ -180,8 +197,8 @@ def parse_http_response(http_response) header = http_response.to_hash # FIXME: this regexp might be incorrect... - if header['set-cookie'] && - header['set-cookie'].first && + if header['set-cookie'] && + header['set-cookie'].first && header['set-cookie'].first =~ /tgt=([^&]+);/ @tgt = $~[1] end @@ -190,14 +207,14 @@ def parse_http_response(http_response) if location =~ /ticket=([^&]+)/ @ticket = $~[1] end - + # Legacy check. CAS Server used to return a 200 (Success) or a 302 (Found) on successful authentication. # This behavior should be deprecated at some point in the future. legacy_valid_ticket = (http_response.kind_of?(Net::HTTPSuccess) || http_response.kind_of?(Net::HTTPFound)) && @ticket.present? - + # If using rubycas-server 1.1.0+ valid_ticket = http_response.kind_of?(Net::HTTPSeeOther) && @ticket.present? - + if !legacy_valid_ticket && !valid_ticket @failure = true # Try to extract the error message -- this only works with RubyCAS-Server. From c55b513c198a02081ede64d52c8aeef9d88430d0 Mon Sep 17 00:00:00 2001 From: akki-ng Date: Tue, 18 Apr 2017 21:33:24 +0530 Subject: [PATCH 2/2] Redirect all url if unauthenticated --- lib/casclient/client.rb | 11 +- lib/casclient/frameworks/rails/filter.rb | 145 ++++++++++++----------- 2 files changed, 85 insertions(+), 71 deletions(-) diff --git a/lib/casclient/client.rb b/lib/casclient/client.rb index fc6fc917..b7a21f56 100644 --- a/lib/casclient/client.rb +++ b/lib/casclient/client.rb @@ -5,7 +5,7 @@ class Client attr_reader :log, :username_session_key, :extra_attributes_session_key attr_reader :ticket_store attr_reader :proxy_host, :proxy_port - attr_writer :login_url, :validate_url, :proxy_url, :logout_url, :service_url + attr_writer :login_url, :validate_url, :proxy_url, :logout_url, :service_url, :redirect_all attr_accessor :proxy_callback_url, :proxy_retrieval_url def initialize(conf = nil) @@ -26,6 +26,7 @@ def configure(conf) @cas_base_url = conf[:cas_base_url].gsub(/\/$/, '') @cas_destination_logout_param_name = conf[:cas_destination_logout_param_name] + @redirect_all = conf[:redirect_all] @login_url = conf[:login_url] @logout_url = conf[:logout_url] @validate_url = conf[:validate_url] @@ -66,6 +67,14 @@ def login_url @login_url || (cas_base_url + "/login") end + def redirect_all + if @redirect_all.present? + return @redirect_all + else + return false + end + end + def validate_url @validate_url || (cas_base_url + "/proxyValidate") end diff --git a/lib/casclient/frameworks/rails/filter.rb b/lib/casclient/frameworks/rails/filter.rb index 2ab71780..f204b06e 100644 --- a/lib/casclient/frameworks/rails/filter.rb +++ b/lib/casclient/frameworks/rails/filter.rb @@ -3,7 +3,7 @@ module Frameworks module Rails class Filter cattr_reader :config, :log, :client, :fake_user, :fake_extra_attributes - + # These are initialized when you call configure. @@config = nil @@client = nil @@ -14,38 +14,38 @@ class Filter def self.before(controller) self.filter controller end - + class << self def filter(controller) raise "Cannot use the CASClient filter because it has not yet been configured." if config.nil? - + if @@fake_user controller.session[client.username_session_key] = @@fake_user controller.session[:casfilteruser] = @@fake_user controller.session[client.extra_attributes_session_key] = @@fake_extra_attributes if @@fake_extra_attributes return true end - + last_st = controller.session[:cas_last_valid_ticket] last_st_service = controller.session[:cas_last_valid_ticket_service] - + if single_sign_out(controller) controller.send(:render, :text => "CAS Single-Sign-Out request intercepted.") - return false + return false end st = read_ticket(controller) - - if st && last_st && - last_st == st.ticket && + + if st && last_st && + last_st == st.ticket && last_st_service == st.service - # warn() rather than info() because we really shouldn't be re-validating the same ticket. - # The only situation where this is acceptable is if the user manually does a refresh and + # warn() rather than info() because we really shouldn't be re-validating the same ticket. + # The only situation where this is acceptable is if the user manually does a refresh and # the same ticket happens to be in the URL. log.warn("Re-using previously validated ticket since the ticket id and service are the same.") return true elsif last_st && - !config[:authenticate_on_every_request] && + !config[:authenticate_on_every_request] && controller.session[client.username_session_key] # Re-use the previous ticket if the user already has a local CAS session (i.e. if they were already # previously authenticated for this service). This is to prevent redirection to the CAS server on every @@ -58,35 +58,35 @@ def filter(controller) "Previous ticket #{last_st.inspect} will be re-used." return true end - + if st client.validate_service_ticket(st) unless st.has_been_validated? - + if st.is_valid? #if is_new_session log.info("Ticket #{st.ticket.inspect} for service #{st.service.inspect} belonging to user #{st.user.inspect} is VALID.") controller.session[client.username_session_key] = st.user.dup controller.session[client.extra_attributes_session_key] = HashWithIndifferentAccess.new(st.extra_attributes) if st.extra_attributes - + if st.extra_attributes log.debug("Extra user attributes provided along with ticket #{st.ticket.inspect}: #{st.extra_attributes.inspect}.") end - + # RubyCAS-Client 1.x used :casfilteruser as it's username session key, # so we need to set this here to ensure compatibility with configurations # built around the old client. controller.session[:casfilteruser] = st.user - + if config[:enable_single_sign_out] client.ticket_store.store_service_session_lookup(st, controller) end #end - + # Store the ticket in the session to avoid re-validating the same service # ticket with the CAS server. controller.session[:cas_last_valid_ticket] = st.ticket controller.session[:cas_last_valid_ticket_service] = st.service - + if st.pgt_iou unless controller.session[:cas_pgt] && controller.session[:cas_pgt].ticket && controller.session[:cas_pgt].iou == st.pgt_iou log.info("Receipt has a proxy-granting ticket IOU. Attempting to retrieve the proxy-granting ticket...") @@ -125,7 +125,7 @@ def filter(controller) log.warn "The CAS client is NOT configured to allow gatewaying, yet this request was gatewayed. Something is not right!" end end - + unauthorized!(controller) return false end @@ -134,17 +134,17 @@ def filter(controller) unauthorized!(controller) return false end - + def configure(config) @@config = config @@config[:logger] = ::Rails.logger unless @@config[:logger] @@client = CASClient::Client.new(config) @@log = client.log end - + # used to allow faking for testing # with cucumber and other tools. - # use like + # use like # CASClient::Frameworks::Rails::Filter.fake("homer") # you can also fake extra attributes by including a second parameter # CASClient::Frameworks::Rails::Filter.fake("homer", {:roles => ['dad', 'husband']}) @@ -152,14 +152,14 @@ def fake(username, extra_attributes = nil) @@fake_user = username @@fake_extra_attributes = extra_attributes end - + def use_gatewaying? @@config[:use_gatewaying] end - - # Returns the login URL for the current controller. + + # Returns the login URL for the current controller. # Useful when you want to provide a "Login" link in a GatewayFilter'ed - # action. + # action. def login_url(controller) service_url = read_service_url(controller) url = client.add_service_to_login_url(service_url) @@ -169,7 +169,7 @@ def login_url(controller) # allow controllers to reuse the existing config to auto-login to # the service - # + # # Use this from within a controller. Pass the controller, the # login-credentials and the path that you want the user # resdirected to on success. @@ -198,20 +198,20 @@ def login_to_service(controller, credentials, return_path) else log.info("Ticket #{resp.ticket.inspect} for service #{return_path.inspect} is VALID.") end - + resp end - - # Clears the given controller's local Rails session, does some local + + # Clears the given controller's local Rails session, does some local # CAS cleanup, and redirects to the CAS logout page. Additionally, the - # request.referer value from the controller instance - # is passed to the CAS server as a 'destination' parameter. This + # request.referer value from the controller instance + # is passed to the CAS server as a 'destination' parameter. This # allows RubyCAS server to provide a follow-up login page allowing - # the user to log back in to the service they just logged out from - # using a different username and password. Other CAS server - # implemenations may use this 'destination' parameter in different - # ways. - # If given, the optional service URL overrides + # the user to log back in to the service they just logged out from + # using a different username and password. Other CAS server + # implemenations may use this 'destination' parameter in different + # ways. + # If given, the optional service URL overrides # request.referer. def logout(controller, service = nil) referer = service || controller.request.referer @@ -220,70 +220,75 @@ def logout(controller, service = nil) controller.send(:reset_session) controller.send(:redirect_to, client.logout_url(referer)) end - + def unauthorized!(controller, vr = nil) format = nil unless controller.request.format.nil? format = controller.request.format.to_sym end - format = (format == :js ? :json : format) - case format - when :xml, :json - if vr - case format - when :xml - controller.send(:render, :xml => { :error => vr.failure_message }.to_xml(:root => 'errors'), :status => :unauthorized) - when :json - controller.send(:render, :json => { :errors => { :error => vr.failure_message }}, :status => :unauthorized) + + if @@client.redirect_all() == true + redirect_to_cas_for_authentication(controller) + else + format = (format == :js ? :json : format) + case format + when :xml, :json + if vr + case format + when :xml + controller.send(:render, :xml => { :error => vr.failure_message }.to_xml(:root => 'errors'), :status => :unauthorized) + when :json + controller.send(:render, :json => { :errors => { :error => vr.failure_message }}, :status => :unauthorized) + end + else + controller.send(:head, :unauthorized) end else - controller.send(:head, :unauthorized) + redirect_to_cas_for_authentication(controller) end - else - redirect_to_cas_for_authentication(controller) end end - + def redirect_to_cas_for_authentication(controller) redirect_url = login_url(controller) - + if use_gatewaying? controller.session[:cas_sent_to_gateway] = true redirect_url << "&gateway=true" else controller.session[:cas_sent_to_gateway] = false end - + if controller.session[:previous_redirect_to_cas] && controller.session[:previous_redirect_to_cas] > (Time.now - 1.second) 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!") controller.session[:cas_validation_retry_count] ||= 0 - + if controller.session[:cas_validation_retry_count] > 3 log.error("Redirection loop intercepted. Client at #{controller.request.remote_ip.inspect} will be redirected back to login page and forced to renew authentication.") redirect_url += "&renew=1&redirection_loop_intercepted=1" end - + controller.session[:cas_validation_retry_count] += 1 else controller.session[:cas_validation_retry_count] = 0 end controller.session[:previous_redirect_to_cas] = Time.now - + log.debug("Redirecting to #{redirect_url.inspect}") - controller.send(:redirect_to, redirect_url) + controller.send(:redirect_to, redirect_url, :status => 307) end - + private def single_sign_out(controller) - + # Avoid calling raw_post (which may consume the post body) if # this seems to be a file upload if content_type = controller.request.headers["CONTENT_TYPE"] && content_type =~ %r{^multipart/} return false end - + if controller.request.post? && controller.params['logoutRequest'] && #This next line checks the logoutRequest value for both its regular and URI.escape'd form. I couldn't get @@ -293,39 +298,39 @@ def single_sign_out(controller) # TODO: Maybe check that the request came from the registered CAS server? Although this might be # pointless since it's easily spoofable... si = $~[1] - + unless config[:enable_single_sign_out] 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)." return false end - + log.debug "Intercepted single-sign-out request for CAS session #{si.inspect}." @@client.ticket_store.process_single_sign_out(si) - + # Return true to indicate that a single-sign-out request was detected # and that further processing of the request is unnecessary. return true end - + # This is not a single-sign-out request. return false end - + def read_ticket(controller) ticket = controller.params[:ticket] - + return nil unless ticket - + log.debug("Request contains ticket #{ticket.inspect}.") - + if ticket =~ /^PT-/ ProxyTicket.new(ticket, read_service_url(controller), controller.params[:renew]) else ServiceTicket.new(ticket, read_service_url(controller), controller.params[:renew]) end end - + def returning_from_gateway?(controller) controller.session[:cas_sent_to_gateway] end