Skip to content

Commit

Permalink
Document how the rack interface works and improve checks on response
Browse files Browse the repository at this point in the history
  • Loading branch information
alexcrichton committed Jul 17, 2011
1 parent 95a93b6 commit 6dcfcd7
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 28 deletions.
56 changes: 34 additions & 22 deletions lib/rack/shibboleth.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,56 @@ class Shibboleth
DS = 'ds:http://www.w3.org/2000/09/xmldsig#'
XENC = 'xenc:http://www.w3.org/2001/04/xmlenc#'

def initialize app, opts = {}
# Creates a new instance of this middleware to be used.
# Required options are:
# * private_key - path to the private key file which is the pair of
# the public key registered with your IdP
# * idp_url - see {Request#idp_url}
# * assertion_url - see {Request#assertion_url}
# * issuer - see {Request#issuer}
#
# Using this middleware, a client will attempt authentication if the
# +/auth/shibboleth+ path is visited. Upon successful authentication, the
# application will be called on the +assertion_url+ path with the
# {Resolver} object located in:
# env['shibboleth.resolver']
#
# This object can be either +nil+ or a {Resolver}
#
# @param app the application to proxy requests to
# @param [Hash] opts a hash of options to this shibboleth instance.
def initialize app, opts
@app = app
@opts = opts

if @opts[:private_key].nil? || !::File.exists?(@opts[:private_key])
raise ArgumentError, 'need valid :private_key option'
end
raise ArgumentError, 'need :idp_url option' if @opts[:idp_url].nil?
raise ArgumentError, 'need :issuer option' if @opts[:issuer].nil?
raise ArgumentError, 'need :assertion_url option' if @opts[:assertion_url].nil?
@private_key = OpenSSL::PKey::RSA.new(::File.read(@opts[:private_key]))
end

def call env
req = Rack::Request.new env

if req.path == '/auth/shibboleth'
response = Rack::Response.new
shib_request = Shibboleth::Request.new @opts

if env['PATH_INFO'] == '/auth/shibboleth'
query = {
:SAMLRequest => shib_request.encode,
:RelayState => "#{req.referer}/auth/shibboleth/callback"
:SAMLRequest => Shibboleth::Request.new(@opts).encode,
:RelayState => @opts[:issuer]
}

arr = query.map{ |k, v| "#{k}=#{Rack::Utils.escape v}" }

response.redirect @opts[:idp_url] + '?' + arr.join('&')
response.finish
elsif req.path == '/Shibboleth.sso/SAML2/POST'
response = Rack::Response.new

resolver = Shibboleth::Resolver.from_response env['rack.input'].read,
@private_key

[200, {'Content-Type' => 'text/plain'},
[resolver.attributes.inspect]]
else
@app.call env
return Rack::Response.new.tap{ |r|
r.redirect @opts[:idp_url] + '?' + arr.join('&')
}.finish
elsif env['PATH_INFO'] == '/Shibboleth.sso/SAML2/POST'
env['shibboleth.resolver'] = Shibboleth::Resolver.from_response(
env['rack.input'].read, @private_key, @opts)
end

@app.call env
end

end
end
end
24 changes: 19 additions & 5 deletions lib/rack/shibboleth/resolver.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,37 @@ class Resolver
# @return [Rack::Shibboleth::Resolver, false] either the resolver object
# for the specified response or false if the response could not
# be decode and/or verified
def self.from_response resp, private_key
def self.from_response resp, private_key, opts
xml = Rack::Utils.parse_query(resp)
xml = Base64.decode64 xml['SAMLResponse']
shib_response = Shibboleth::Response.new xml

assertion = shib_response.decode private_key

assertion ? Resolver.new(assertion) : assertion
if assertion
resolver = Resolver.new assertion, opts
resolver.valid? ? resolver : nil
end
end

# Creates a new resolver for the specified assertion document which was
# decoded from a response to the IdP
#
# @param [LibXML::XML::Document] assertion the parsed version of the
# xml received from the IdP
def initialize assertion
@doc = assertion
# @param [Hash] opts options specified to {Rack::Shibboleth}
def initialize assertion, opts
@doc = assertion
@opts = opts
end

# Tests whether this response from the IdP is valid based on the
# conditions specified.
#
# @return [Boolean] true if the resolver has valid attributes.
def valid?
conds = conditions
conds[:after] < Time.now && Time.now < conds[:before] &&
conds[:audience] == @opts[:issuer]
end

# The exact time that the response was issued at
Expand Down
7 changes: 6 additions & 1 deletion spec/rack/shibboleth/resolver_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@

context 'parsing a sample response' do

before do
Time.stub(:now).and_return Time.utc(2011, 3, 1, 15, 55)
end

let(:response) { Rack::Test.read_fixture 'sample-response' }
subject{
Rack::Shibboleth::Resolver.from_response response, Rack::Test.sample_key
Rack::Shibboleth::Resolver.from_response response, Rack::Test.sample_key,
{:issuer => 'https://mirror.alexcrichton.com/shibboleth-sp'}
}

its(:issued) { should be_an_instance_of(Time) }
Expand Down

0 comments on commit 6dcfcd7

Please sign in to comment.