Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

eIDAS SAML samlp:Extensions to AuthRequest option #180

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 35 additions & 16 deletions lib/omniauth/strategies/saml.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,28 +15,47 @@ def self.inherited(subclass)
option :name_identifier_format, nil
option :idp_sso_target_url_runtime_params, {}
option :request_attributes, [
{ :name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address' },
{ :name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name' },
{ :name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name' },
{ :name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name' }
{:name => 'email', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Email address'},
{:name => 'name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Full name'},
{:name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name'},
{:name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name'}
]
option :attribute_service_name, 'Required attributes'
option :attribute_statements, {
name: ["name"],
email: ["email", "mail"],
first_name: ["first_name", "firstname", "firstName"],
last_name: ["last_name", "lastname", "lastName"]
name: ["name"],
email: ["email", "mail"],
first_name: ["first_name", "firstname", "firstName"],
last_name: ["last_name", "lastname", "lastName"]
}
option :slo_default_relay_state
option :uid_attribute
option :auth_request_include_request_attributes, false
option :sptype, false
option :idp_slo_session_destroy, proc { |_env, session| session.clear }

def request_phase
authn_request = OneLogin::RubySaml::Authrequest.new

with_settings do |settings|
redirect(authn_request.create(settings, additional_params_for_authn_request))
options[:assertion_consumer_service_url] ||= callback_url
settings = OneLogin::RubySaml::Settings.new(options)

if options[:sptype] != false
settings.extensions[:sptype] = options[:sptype]
end
if options[:auth_request_include_request_attributes] == true
settings.extensions[:requested_attributes] = with_requested_attributes
end

redirect(authn_request.create(settings, additional_params_for_authn_request))
end

def with_requested_attributes
raise OmniAuth::Strategies::SAML::ValidationError.new('Cannot convert option request_attributes to samlp:Extensions/eidas:RequestedAttributes') unless options[:request_attributes].respond_to? :each
attrs = []
options[:request_attributes].each do |orig_attr|
attrs.push(OneLogin::RubySaml::RequestedAttribute.new({:Name => orig_attr[:name], :FriendlyName => orig_attr[:friendly_name], :NameFormat => orig_attr[:name_format], :isRequired => orig_attr[:required] || false}))
end
attrs
end

def callback_phase
Expand All @@ -61,7 +80,7 @@ def response_fingerprint
response = request.params["SAMLResponse"]
response = (response =~ /^</) ? response : Base64.decode64(response)
document = XMLSecurity::SignedDocument::new(response)
cert_element = REXML::XPath.first(document, "//ds:X509Certificate", { "ds"=> 'http://www.w3.org/2000/09/xmldsig#' })
cert_element = REXML::XPath.first(document, "//ds:X509Certificate", {"ds" => 'http://www.w3.org/2000/09/xmldsig#'})
base64_cert = cert_element.text
cert_text = Base64.decode64(base64_cert)
cert = OpenSSL::X509::Certificate.new(cert_text)
Expand Down Expand Up @@ -108,7 +127,7 @@ def other_phase
Hash[found_attributes]
end

extra { { :raw_info => @attributes, :session_index => @session_index, :response_object => @response_object } }
extra { {:raw_info => @attributes, :session_index => @session_index, :response_object => @response_object} }

def find_attribute_by(keys)
keys.each do |key|
Expand Down Expand Up @@ -180,7 +199,7 @@ def handle_logout_request(raw_request, settings)
logout_request = OneLogin::RubySaml::SloLogoutrequest.new(raw_request)

if logout_request.is_valid? &&
logout_request.name_id == session["saml_uid"]
logout_request.name_id == session["saml_uid"]

# Actually log out this session
options[:idp_slo_session_destroy].call @env, session
Expand Down Expand Up @@ -231,7 +250,7 @@ def validate_fingerprint(settings)

def options_for_response_object
# filter options to select only extra parameters
opts = options.select {|k,_| RUBYSAML_RESPONSE_OPTIONS.include?(k.to_sym)}
opts = options.select { |k, _| RUBYSAML_RESPONSE_OPTIONS.include?(k.to_sym) }

# symbolize keys without activeSupport/symbolize_keys (ruby-saml use symbols)
opts.inject({}) do |new_hash, (key, value)|
Expand All @@ -247,7 +266,7 @@ def other_phase_for_metadata

add_request_attributes_to(settings) if options.request_attributes.length > 0

Rack::Response.new(response.generate(settings), 200, { "Content-Type" => "application/xml" }).finish
Rack::Response.new(response.generate(settings), 200, {"Content-Type" => "application/xml"}).finish
end
end

Expand All @@ -269,7 +288,7 @@ def other_phase_for_spslo
redirect(generate_logout_request(settings))
end
else
Rack::Response.new("Not Implemented", 501, { "Content-Type" => "text/html" }).finish
Rack::Response.new("Not Implemented", 501, {"Content-Type" => "text/html"}).finish
end
end

Expand Down
32 changes: 31 additions & 1 deletion spec/omniauth/strategies/saml_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ def post_xml(xml=:example_response, opts = {})
{ :name => 'first_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Given name' },
{ :name => 'last_name', :name_format => 'urn:oasis:names:tc:SAML:2.0:attrname-format:basic', :friendly_name => 'Family name' }
],
:attribute_service_name => 'Required attributes'
:attribute_service_name => 'Required attributes',
:sptype => false,
:auth_request_include_request_attributes => false
}
end
let(:strategy) { [OmniAuth::Strategies::SAML, saml_options] }
Expand Down Expand Up @@ -115,6 +117,33 @@ def post_xml(xml=:example_response, opts = {})
expect(query['SigAlg']).to eq XMLSecurity::Document::RSA_SHA256
end
end

context 'when using eidas extensions in authn request' do
subject { get '/auth/saml' }

before do
saml_options[:compress_request] = false

saml_options[:sptype] = 'private'
saml_options[:auth_request_include_request_attributes] = true
end

it "should contain correct sptype and RequestedAttributes" do
is_expected.to be_redirect

location = URI.parse(last_response.location)
query = Rack::Utils.parse_query location.query
expect(query).to have_key('SAMLRequest')

request = REXML::Document.new(Base64.decode64(query['SAMLRequest']))
request.elements.each('/samlp:AuthnRequest/samlp:Extensions/eidas:SPType') do |element|
expect(element.text).to match /private/
end
request.elements.each('/samlp:AuthnRequest/samlp:Extensions/eidas:RequestedAttributes/eidas:RequestedAttribute') do |element|
expect(element.attributes['isRequired']).to match /false/
end
end
end
end

describe 'POST /auth/saml/callback' do
Expand Down Expand Up @@ -450,4 +479,5 @@ def test_default_relay_state(static_default_relay_state = nil, &block_default_re
expect(OmniAuth.strategies).to include(described_class, subclass)
end
end

end