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