Skip to content

Add support for SAML 1 Tokens #12

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: development
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 ##

Expand Down
7 changes: 6 additions & 1 deletion lib/omniauth/strategies/wsfed.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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'])
Expand All @@ -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)
Expand Down
48 changes: 16 additions & 32 deletions lib/omniauth/strategies/wsfed/auth_callback.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand All @@ -92,6 +65,17 @@ def name_id

private

def token
@token ||= begin
case settings[:saml_version]
when '1'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you set saml_version as integer, this will not work, it would be better to case settings[:saml_version].to_s

SAML1Token.new(document)
else
SAML2Token.new(document)
end
end
end


# WS-Trust token lifetime element
def wstrust_lifetime
Expand Down
45 changes: 45 additions & 0 deletions lib/omniauth/strategies/wsfed/saml_1_token.rb
Original file line number Diff line number Diff line change
@@ -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
45 changes: 45 additions & 0 deletions lib/omniauth/strategies/wsfed/saml_2_token.rb
Original file line number Diff line number Diff line change
@@ -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
10 changes: 7 additions & 3 deletions lib/omniauth/strategies/wsfed/xml_security.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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]}']")
id = saml_version == '1' ? 'AssertionID' : 'ID'
hashed_element = REXML::XPath.first(self, "//[@#{id}='#{uri[1,uri.size]}']")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that checking both would be the fastest way to solve this

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)
Expand Down
23 changes: 17 additions & 6 deletions spec/omniauth/strategies/wsfed/auth_callback_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 = [
Expand Down
66 changes: 66 additions & 0 deletions spec/support/saml1_example.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<t:RequestSecurityTokenResponse xmlns:t="http://schemas.xmlsoap.org/ws/2005/02/trust">
<t:Lifetime>
<wsu:Created xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-06-27T19:45:38.263Z</wsu:Created>
<wsu:Expires xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">2014-06-27T20:45:38.263Z</wsu:Expires>
</t:Lifetime>
<wsp:AppliesTo xmlns:wsp="http://schemas.xmlsoap.org/ws/2004/09/policy">
<wsa:EndpointReference xmlns:wsa="http://www.w3.org/2005/08/addressing">
<wsa:Address>http://rp.coding4streetcred.com/sample</wsa:Address>
</wsa:EndpointReference>
</wsp:AppliesTo>
<t:RequestedSecurityToken>
<saml:Assertion MajorVersion="1" MinorVersion="1" AssertionID="_fa0de02b-b5a1-49c5-a8c0-4b391295a789" Issuer="https://c4sc-identity.accesscontrol.windows.net/" IssueInstant="2014-06-27T19:45:38.263Z" xmlns:saml="urn:oasis:names:tc:SAML:1.0:assertion">
<saml:Conditions NotBefore="2014-06-27T19:45:38.263Z" NotOnOrAfter="2014-06-27T20:45:38.263Z">
<saml:AudienceRestrictionCondition>
<saml:Audience>https://c4sc-identity.accesscontrol.windows.net</saml:Audience>
</saml:AudienceRestrictionCondition>
</saml:Conditions>
<saml:AttributeStatement>
<saml:Subject>
<saml:SubjectConfirmation>
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
</saml:SubjectConfirmation>
</saml:Subject>
<saml:Attribute AttributeName="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>[email protected]</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name" AttributeNamespace="http://schemas.microsoft.com/ws/2008/06/identity/claims">
<saml:AttributeValue>kbeckman.c4sc</saml:AttributeValue>
</saml:Attribute>
<saml:Attribute AttributeName="http://schemas.microsoft.com/accesscontrolservice/2010/07/claims/identityprovider" AttributeNamespace="http://schemas.xmlsoap.org/ws/2005/05/identity/claims">
<saml:AttributeValue>http://identity.c4sc.com/trust/</saml:AttributeValue>
</saml:Attribute>
</saml:AttributeStatement>
<saml:AuthenticationStatement AuthenticationMethod="urn:oasis:names:tc:SAML:1.0:am:password" AuthenticationInstant="2014-06-27T19:45:38.232Z">
<saml:Subject>
<saml:SubjectConfirmation>
<saml:ConfirmationMethod>urn:oasis:names:tc:SAML:1.0:cm:bearer</saml:ConfirmationMethod>
</saml:SubjectConfirmation>
</saml:Subject>
</saml:AuthenticationStatement>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<ds:Reference URI="#_fa0de02b-b5a1-49c5-a8c0-4b391295a789">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
<ds:Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<ds:DigestValue>bdwpOR25Tiw03Y5gZsz/NDSrN2T1XAEUQl9/B2aDVjs=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>O3dJ5YtFIJJHk8SKAqdI2goSJUj7/oZebGwrm5yjVz8WT9TdHfJT2e/rygKLz9MBujZoZ13oGaVq6NVJLvmvR+IrKsUIuUeXwk4X2UexYxJL9VGZD6RnXR+p0Jne+jGUIlVOb2zMr29Ew27wLfnw3za+Zf5ravQZ/bv3LoL/LFIYFb7iR4XlJ5bjlMhO41euUp/6NTntIC90utugpjqcPryxNbIto6nk3w57IrKmw9rFpRJudoXbw7BsA3t69dmzu2MQzjILbFcfmkUgtEXDQyGM/ziXqxNFEGNHkycEsO37NO4/t5Hk1zPufBbbhSm+5K6tVqZ2Nl1e5yNciBwo6g==</ds:SignatureValue>
<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
<X509Data>
<X509Certificate>MIIDHDCCAgSgAwIBAgIQXMOBsrQ1QJpNmYFiiW5+PjANBgkqhkiG9w0BAQUFADAyMTAwLgYDVQQDEydjNHNjLWlkZW50aXR5LmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwHhcNMTIwNjI5MjAzNTA2WhcNMTMwNjMwMDIzNTA2WjAyMTAwLgYDVQQDEydjNHNjLWlkZW50aXR5LmFjY2Vzc2NvbnRyb2wud2luZG93cy5uZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrieSwMYpW73fJtHiBw1yQFWcFZwvDbltFfdb4xzC+MuC8KU/QJzKGBzxixwmvTUKTbH4W4gKNzi7+/gGk9my1UFnDLsnJBooGjx6lCXMU9HoOQA0tXVGkyYeD11Lb2KYWoivvGFI6dun84JY1n1hbAnuyqr+8VUfKpBk3bO6s3xLf2eojAHKhUiw2E+ZBFZWqlTMtSoNupe8I4Zs5Kkp5Xe1hrDjCzHWTHRf880y8f6KOvieQuGO2a2dBSYJVY3IHr1cLlk/o9Dwks4zSjYDABE8NDKer7aq2pnXl0/0XeXeYsDsZFrwfuRtf06pqGgMuqo1aFRiQ2+gm4naZn7D3AgMBAAGjLjAsMAsGA1UdDwQEAwIE8DAdBgNVHQ4EFgQUIBxPC4SJIAWTt6Q4htDcvHDgatAwDQYJKoZIhvcNAQEFBQADggEBAB2RNPpJMNotdKMKtQkV/tEhttOOq+bXlMa42UQu6r+ikgJ6WcSecBxOs4KHw3lj7wO0l8CIOfvXy5KBePLQsUuk8tWCdKdpa+7uuGntIHdlTAvjlcVhXXAQQvyS+wUbnwj8rCN85e76EWMWCAVXzqwMURt5Rzmb+SdU40hRi+7u+HmZaQW5kDXD3CIm+1eOU7oyWpz6Ltx1DEJ88qt4xB2e6IGQUTdvq2jUnyzC1f9jdtRtZ3LiiVkA29rfRROJQb/PeEinUON0hP7/6VS9LZPL4vB1EIOBNahY1V4/75Hb+NGzb05w71hMUiJK2eu8w1WwqcRqUlu7GI921Od1oFQ=</X509Certificate>
</X509Data>
</KeyInfo>
</ds:Signature>
</saml:Assertion>
</t:RequestedSecurityToken>
<t:TokenType>urn:oasis:names:tc:SAML:1.0:assertion</t:TokenType>
<t:RequestType>http://schemas.xmlsoap.org/ws/2005/02/trust/Issue</t:RequestType>
<t:KeyType>http://schemas.xmlsoap.org/ws/2005/05/identity/NoProofKey</t:KeyType>
</t:RequestSecurityTokenResponse>