Skip to content

eIDAS SAML samlp:Extensions to AuthRequest option #180

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

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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
@@ -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
@@ -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)
@@ -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|
@@ -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
@@ -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)|
@@ -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

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

32 changes: 31 additions & 1 deletion spec/omniauth/strategies/saml_spec.rb
Original file line number Diff line number Diff line change
@@ -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] }
@@ -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
@@ -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