diff --git a/README.md b/README.md index b817326..485ba2d 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,8 @@ posted. **Required** * `:id_claim` - Name of the authentication claim that you want to use as OmniAuth's **uid** property. +* `:saml_version` - The version of SAML tokens. **Defaults to 2**. + ## Authors and Credits ## diff --git a/lib/omniauth/strategies/wsfed.rb b/lib/omniauth/strategies/wsfed.rb index 4154491..783b08f 100644 --- a/lib/omniauth/strategies/wsfed.rb +++ b/lib/omniauth/strategies/wsfed.rb @@ -9,9 +9,14 @@ class WSFed autoload :AuthRequest, 'omniauth/strategies/wsfed/auth_request' autoload :AuthCallback, 'omniauth/strategies/wsfed/auth_callback' autoload :AuthCallbackValidator, 'omniauth/strategies/wsfed/auth_callback_validator' + autoload :SAML2Token, 'omniauth/strategies/wsfed/saml_2_token' + autoload :SAML1Token, 'omniauth/strategies/wsfed/saml_1_token' autoload :ValidationError, 'omniauth/strategies/wsfed/validation_error' autoload :XMLSecurity, 'omniauth/strategies/wsfed/xml_security' + WS_TRUST = 'http://schemas.xmlsoap.org/ws/2005/02/trust' + WS_POLICY = 'http://schemas.xmlsoap.org/ws/2004/09/policy' + # Issues passive WS-Federation redirect for authentication... def request_phase auth_request = OmniAuth::Strategies::WSFed::AuthRequest.new(options, :whr => @request.params['whr']) @@ -25,7 +30,7 @@ def callback_phase wsfed_callback = request.params['wresult'] - signed_document = OmniAuth::Strategies::WSFed::XMLSecurity::SignedDocument.new(wsfed_callback) + signed_document = OmniAuth::Strategies::WSFed::XMLSecurity::SignedDocument.new(wsfed_callback, options) signed_document.validate(get_fingerprint, false) auth_callback = OmniAuth::Strategies::WSFed::AuthCallback.new(wsfed_callback, options) diff --git a/lib/omniauth/strategies/wsfed/auth_callback.rb b/lib/omniauth/strategies/wsfed/auth_callback.rb index c8e8177..a92613a 100644 --- a/lib/omniauth/strategies/wsfed/auth_callback.rb +++ b/lib/omniauth/strategies/wsfed/auth_callback.rb @@ -8,9 +8,7 @@ class WSFed class AuthCallback - WS_TRUST = 'http://schemas.xmlsoap.org/ws/2005/02/trust' WS_UTILITY = 'http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd' - WS_POLICY = 'http://schemas.xmlsoap.org/ws/2004/09/policy' attr_accessor :options, :raw_callback, :settings @@ -27,17 +25,14 @@ def initialize(raw_callback, settings, options = {}) # TODO: remove reference to SignedDocument (document) and move it to validation # use response variable instead... def document - @document ||= OmniAuth::Strategies::WSFed::XMLSecurity::SignedDocument.new(raw_callback) + @document ||= OmniAuth::Strategies::WSFed::XMLSecurity::SignedDocument.new(raw_callback, settings) end # WS-Trust Envelope and WS* Element Values def audience - @audience ||= begin - applies_to = REXML::XPath.first(document, '//t:RequestSecurityTokenResponse/wsp:AppliesTo', { 't' => WS_TRUST, 'wsp' => WS_POLICY }) - REXML::XPath.first(applies_to, '//EndpointReference/Address').text - end + @audience ||= token.audience end def created_at @@ -49,36 +44,14 @@ def expires_at end - # SAML 2.0 Assertion [Token] Values - # Note: If/When future development warrants additional token types, these items should be refactored into a - # token abstraction... + # Token Values def issuer - @issuer ||= begin - REXML::XPath.first(document, '//Assertion/Issuer').text - end + @issuer ||= token.issuer end def claims - @attr_statements ||= begin - stmt_element = REXML::XPath.first(document, '//Assertion/AttributeStatement') - return {} if stmt_element.nil? - - {}.tap do |result| - stmt_element.elements.each do |attr_element| - name = attr_element.attributes['Name'] - - if attr_element.elements.count > 1 - value = [] - attr_element.elements.each { |element| value << element.text } - else - value = attr_element.elements.first.text.lstrip.rstrip - end - - result[name] = value - end - end - end + @claims ||= token.claims end alias :attributes :claims @@ -92,6 +65,17 @@ def name_id private + def token + @token ||= begin + case settings[:saml_version].to_s + when '1' + SAML1Token.new(document) + else + SAML2Token.new(document) + end + end + end + # WS-Trust token lifetime element def wstrust_lifetime diff --git a/lib/omniauth/strategies/wsfed/saml_1_token.rb b/lib/omniauth/strategies/wsfed/saml_1_token.rb new file mode 100644 index 0000000..ccc6d9c --- /dev/null +++ b/lib/omniauth/strategies/wsfed/saml_1_token.rb @@ -0,0 +1,45 @@ +module OmniAuth + module Strategies + class WSFed + class SAML1Token + + attr_accessor :document + + def initialize(document) + @document = document + end + + def audience + applies_to = REXML::XPath.first(document, '//t:RequestSecurityTokenResponse/wsp:AppliesTo', { 't' => WS_TRUST, 'wsp' => WS_POLICY }) + REXML::XPath.first(applies_to, '//wsa:EndpointReference/wsa:Address').text + end + + def issuer + REXML::XPath.first(document, '//saml:Assertion').attributes['Issuer'] + end + + def claims + stmt_element = REXML::XPath.first(document, '//saml:Assertion/saml:AttributeStatement') + + return {} if stmt_element.nil? + + {}.tap do |result| + stmt_element.each_element('saml:Attribute') do |attr_element| + name = attr_element.attributes['AttributeName'] + + if attr_element.elements.count > 1 + value = [] + attr_element.elements.each { |element| value << element.text } + else + value = attr_element.elements.first.text.lstrip.rstrip + end + + result[name] = value + end + end + end + + end + end + end +end diff --git a/lib/omniauth/strategies/wsfed/saml_2_token.rb b/lib/omniauth/strategies/wsfed/saml_2_token.rb new file mode 100644 index 0000000..d84600d --- /dev/null +++ b/lib/omniauth/strategies/wsfed/saml_2_token.rb @@ -0,0 +1,45 @@ +module OmniAuth + module Strategies + class WSFed + class SAML2Token + + attr_accessor :document + + def initialize(document) + @document = document + end + + def audience + applies_to = REXML::XPath.first(document, '//t:RequestSecurityTokenResponse/wsp:AppliesTo', { 't' => WS_TRUST, 'wsp' => WS_POLICY }) + REXML::XPath.first(applies_to, '//EndpointReference/Address').text + end + + def issuer + REXML::XPath.first(document, '//Assertion/Issuer').text + end + + def claims + stmt_element = REXML::XPath.first(document, '//Assertion/AttributeStatement') + + return {} if stmt_element.nil? + + {}.tap do |result| + stmt_element.elements.each do |attr_element| + name = attr_element.attributes['Name'] + + if attr_element.elements.count > 1 + value = [] + attr_element.elements.each { |element| value << element.text } + else + value = attr_element.elements.first.text.lstrip.rstrip + end + + result[name] = value + end + end + end + + end + end + end +end diff --git a/lib/omniauth/strategies/wsfed/xml_security.rb b/lib/omniauth/strategies/wsfed/xml_security.rb index 3bcf475..51a8eff 100644 --- a/lib/omniauth/strategies/wsfed/xml_security.rb +++ b/lib/omniauth/strategies/wsfed/xml_security.rb @@ -39,11 +39,13 @@ module XMLSecurity class SignedDocument < REXML::Document DSIG = "http://www.w3.org/2000/09/xmldsig#" - attr_accessor :signed_element_id + attr_accessor :signed_element_id, :settings - def initialize(response) + def initialize(response, settings = {}) super(response) extract_signed_element_id + + self.settings = settings end def validate(idp_cert_fingerprint, soft = true) @@ -80,9 +82,11 @@ def validate_doc(base64_cert, soft = true) sig_element.remove # check digests + saml_version = settings[:saml_version] REXML::XPath.each(sig_element, "//ds:Reference", {"ds"=>DSIG}) do |ref| uri = ref.attributes.get_attribute("URI").value - hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']") + hashed_element = REXML::XPath.first(self, "//[@ID='#{uri[1,uri.size]}']") || + REXML::XPath.first(self, "//[@AssertionID='#{uri[1,uri.size]}']") canoner = XML::Util::XmlCanonicalizer.new(false, true) canoner.inclusive_namespaces = inclusive_namespaces if canoner.respond_to?(:inclusive_namespaces) && !inclusive_namespaces.empty? canon_hashed_element = canoner.canonicalize(hashed_element) diff --git a/spec/omniauth/strategies/wsfed/auth_callback_spec.rb b/spec/omniauth/strategies/wsfed/auth_callback_spec.rb index 3d19afe..ed10581 100644 --- a/spec/omniauth/strategies/wsfed/auth_callback_spec.rb +++ b/spec/omniauth/strategies/wsfed/auth_callback_spec.rb @@ -36,16 +36,13 @@ auth_callback.expires_at.should == Time.parse('2012-06-29T21:17:14.766Z') end + end + + shared_examples_for 'SAML token' do it 'should extract the token audience' do auth_callback.audience.should == 'http://rp.coding4streetcred.com/sample' end - end - - context 'SAML 2.0 Assertion [Token] Values' do - - let(:auth_callback) { described_class.new(load_support_xml(:acs_example), @wsfed_settings) } - it 'should extract the issuer' do auth_callback.issuer.should == 'https://c4sc-identity.accesscontrol.windows.net/' end @@ -59,6 +56,20 @@ auth_callback.attributes.should == expected_claims end + end + + context 'SAML 1.0 Assertion [Token] Values' do + + let(:auth_callback) { described_class.new(load_support_xml(:saml1_example), @wsfed_settings.merge(saml_version: '1')) } + + it_behaves_like 'SAML token' + end + + context 'SAML 2.0 Assertion [Token] Values' do + + let(:auth_callback) { described_class.new(load_support_xml(:acs_example), @wsfed_settings) } + + it_behaves_like 'SAML token' it 'should load the proper value from various id_claim settings' do id_claims = [ diff --git a/spec/support/saml1_example.xml b/spec/support/saml1_example.xml new file mode 100644 index 0000000..58fef13 --- /dev/null +++ b/spec/support/saml1_example.xml @@ -0,0 +1,66 @@ + + + 2014-06-27T19:45:38.263Z + 2014-06-27T20:45:38.263Z + + + + http://rp.coding4streetcred.com/sample + + + + + + + https://c4sc-identity.accesscontrol.windows.net + + + + + + urn:oasis:names:tc:SAML:1.0:cm:bearer + + + + kbeckman.c4sc@gmail.com + + + kbeckman.c4sc + + + http://identity.c4sc.com/trust/ + + + + + + urn:oasis:names:tc:SAML:1.0:cm:bearer + + + + + + + + + + + + + + bdwpOR25Tiw03Y5gZsz/NDSrN2T1XAEUQl9/B2aDVjs= + + + O3dJ5YtFIJJHk8SKAqdI2goSJUj7/oZebGwrm5yjVz8WT9TdHfJT2e/rygKLz9MBujZoZ13oGaVq6NVJLvmvR+IrKsUIuUeXwk4X2UexYxJL9VGZD6RnXR+p0Jne+jGUIlVOb2zMr29Ew27wLfnw3za+Zf5ravQZ/bv3LoL/LFIYFb7iR4XlJ5bjlMhO41euUp/6NTntIC90utugpjqcPryxNbIto6nk3w57IrKmw9rFpRJudoXbw7BsA3t69dmzu2MQzjILbFcfmkUgtEXDQyGM/ziXqxNFEGNHkycEsO37NO4/t5Hk1zPufBbbhSm+5K6tVqZ2Nl1e5yNciBwo6g== + + + MIIDHDCCAgSgAwIBAgIQXMOBsrQ1QJpNmYFiiW5+PjANBgkqhkiG9w0BAQUFADAyMTAwLgYDVQQDEydjNHNjLWlkZW50aXR5LmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjI5MjAzNTA2WhcNMTMwNjMwMDIzNTA2WjAyMTAwLgYDVQQDEydjNHNjLWlkZW50aXR5LmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrieSwMYpW73fJtHiBw1yQFWcFZwvDbltFfdb4xzC+MuC8KU/QJzKGBzxixwmvTUKTbH4W4gKNzi7+/gGk9my1UFnDLsnJBooGjx6lCXMU9HoOQA0tXVGkyYeD11Lb2KYWoivvGFI6dun84JY1n1hbAnuyqr+8VUfKpBk3bO6s3xLf2eojAHKhUiw2E+ZBFZWqlTMtSoNupe8I4Zs5Kkp5Xe1hrDjCzHWTHRf880y8f6KOvieQuGO2a2dBSYJVY3IHr1cLlk/o9Dwks4zSjYDABE8NDKer7aq2pnXl0/0XeXeYsDsZFrwfuRtf06pqGgMuqo1aFRiQ2+gm4naZn7D3AgMBAAGjLjAsMAsGA1UdDwQEAwIE8DAdBgNVHQ4EFgQUIBxPC4SJIAWTt6Q4htDcvHDgatAwDQYJKoZIhvcNAQEFBQADggEBAB2RNPpJMNotdKMKtQkV/tEhttOOq+bXlMa42UQu6r+ikgJ6WcSecBxOs4KHw3lj7wO0l8CIOfvXy5KBePLQsUuk8tWCdKdpa+7uuGntIHdlTAvjlcVhXXAQQvyS+wUbnwj8rCN85e76EWMWCAVXzqwMURt5Rzmb+SdU40hRi+7u+HmZaQW5kDXD3CIm+1eOU7oyWpz6Ltx1DEJ88qt4xB2e6IGQUTdvq2jUnyzC1f9jdtRtZ3LiiVkA29rfRROJQb/PeEinUON0hP7/6VS9LZPL4vB1EIOBNahY1V4/75Hb+NGzb05w71hMUiJK2eu8w1WwqcRqUlu7GI921Od1oFQ= + + + + + + urn:oasis:names:tc:SAML:1.0:assertion + http://schemas.xmlsoap.org/ws/2005/02/trust/Issue + http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey +