I've used your gem in another project with success!! Thanks so much. However, I have run into snags with my current client so attempted to deconstruct the gem and write my own builder. The client is using a componentspace and the error messages are not terribly useful. I assume it's something to do with my signature value but was hoping to get another set of eyes. Something is up with the order in which I'm encoding the xml.
# frozen_string_literal: true
# SHA1 the canonical version of the Assertion.
# Generate a SignedInfo XML fragment with the SHA1 signature
# Sign the SignedInfo XML fragment, again the canonical form
# Take the SignedInfo, the Signature and the key info and create a Signature XML fragment
# Insert this SignatureXML into the Assertion (should go right before the saml:subject)
# Now take the assertion(with the signature included) and insert it into the Response
# SHA1 this response
# Generate a SignedInfo XML fragment with the SHA1 signature
# Sign the SignedInfo XML fragment, again the canonical form
# Take the SignedInfo, the Signature and the key info and create a Signature XML fragment
# Insert this SignatureXML into the Response
# Add the XML version info to the response.
module Client
class LoginService
def issuer_url
'localhost:3000'
end
def destination_url
"#{Settings.entity.base_url}/api/rest/v2/authentication/saml"
end
def cert
'config/idp/test.crt'
end
def secret_key
'config/idp/secret_test.pem'
end
def name_id
'jhblacklock@gmail.com'
end
def cert
OpenSSL::X509::Certificate.new(File.read(cert)).to_s
.gsub(/-----BEGIN CERTIFICATE-----/, '')
.gsub(/-----END CERTIFICATE-----/, '')
.delete("\n").strip
end
def iso
yield.iso8601
end
def now
@now ||= Time.now.utc
end
def not_on_or_after
iso { now + 3 * 60 }
end
def not_before
iso { now - 5 }
end
def now_iso
iso { now }
end
def digest_method
'http://www.w3.org/2000/09/xmldsig#sha1'
end
def digest_algorithm
OpenSSL::Digest::SHA1.new
end
def signature(raw)
encoded(raw).delete("\n")
end
def encoded(raw)
key = OpenSSL::PKey::RSA.new(File.open(secret_key), '')
Base64.strict_encode64(key.sign(OpenSSL::Digest::SHA1.new, raw))
end
def raw_file
builder(signed: false).delete("\n")
end
def raw_assertion
assertion_builder(signed: false).delete("\n")
end
def signed
builder(signed: true).delete("\n")
end
def digest_value(xml_raw)
digest(xml_raw).strip
end
def digest(xml_raw)
noko_raw = Nokogiri::XML::Document.parse(xml_raw)
inclusive_namespaces = []
canon_algorithm = Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
canon_hashed_element = noko_raw.canonicalize(canon_algorithm, inclusive_namespaces)
hash = digest_algorithm.digest(canon_hashed_element)
Base64.strict_encode64(hash).delete("\n")
end
def encode_signed_message(signed)
Base64.strict_encode64(signed)
end
def reference_id
@reference_string ||= SecureRandom.uuid
end
def reference_string
@reference_string ||= "id#{reference_id}"
end
def response_id
@response_id ||= SecureRandom.uuid
end
def response_id_string
@response_id_string ||= "id#{response_id}"
end
def assertion_builder(signed: false)
<<~XML
<saml2:Assertion
ID="#{reference_string}"
IssueInstant="#{now_iso}"
Version="2.0"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
>
<saml2:Issuer
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">#{issuer_url}</saml2:Issuer>
#{signature_builder(raw_assertion, reference_string) if signed}
<saml2:Subject xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">#{name_id}</saml2:NameID>
<saml2:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
<saml2:SubjectConfirmationData NotOnOrAfter="#{not_on_or_after}" Recipient="#{destination_url}" />
</saml2:SubjectConfirmation>
</saml2:Subject>
<saml2:Conditions NotBefore="#{not_before}" NotOnOrAfter="#{not_on_or_after}" xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion">
<saml2:AudienceRestriction>
<saml2:Audience>#{destination_url}</saml2:Audience>
</saml2:AudienceRestriction>
</saml2:Conditions>
<saml2:AuthnStatement
AuthnInstant="#{now_iso}"
SessionIndex="#{reference_string}"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
>
<saml2:AuthnContext>
<saml2:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml2:AuthnContextClassRef>
</saml2:AuthnContext>
</saml2:AuthnStatement>
</saml2:Assertion>
XML
end
def signed_info_builder(raw, id_string)
<<~XML
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
<ds:Reference URI="#{id_string}">
<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="#{digest_method}" />
<ds:DigestValue>#{digest_value(raw)}</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
XML
end
def signature_builder(raw, id_string)
<<~XML
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
#{signed_info_builder(raw, id_string)}
#{signature_value_builder(raw, id_string)}
<ds:KeyInfo>
<ds:X509Data>#{certificate_builder}</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>
XML
end
def certificate_builder
<<~XML
<ds:X509Certificate>#{cert}</ds:X509Certificate>
XML
end
def signature_value_builder(raw, _id_string)
<<~XML
<ds:SignatureValue>#{signature(raw).strip}</ds:SignatureValue>
XML
end
def build
signed.delete("\n")
end
def status_builder
<<~XML
<saml2p:Status xmlns:saml2p="urn:oasis:names:tc:SAML:2.0:protocol">
<saml2p:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
</saml2p:Status>
XML
end
def builder(signed: false)
resp_options = {}
resp_options[:Destination] = destination_url
resp_options[:ID] = response_id_string
resp_options[:IssueInstant] = now_iso
resp_options[:Version] = '2.0'
resp_options['xmlns:saml2p'] = 'urn:oasis:names:tc:SAML:2.0:protocol'
builder = Builder::XmlMarkup.new
builder.tag! 'saml2p:Response', resp_options do |response|
response << issuer_builder
response << signature_builder(raw_file, response_id_string) if signed
response << status_builder
response << assertion_builder(signed: true)
end
end
def issuer_builder
<<~XML
<saml2:Issuer
Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity"
xmlns:saml2="urn:oasis:names:tc:SAML:2.0:assertion"
>#{issuer_url}</saml2:Issuer>
XML
end
end
end
I've used your gem in another project with success!! Thanks so much. However, I have run into snags with my current client so attempted to deconstruct the gem and write my own builder. The client is using a componentspace and the error messages are not terribly useful. I assume it's something to do with my signature value but was hoping to get another set of eyes. Something is up with the order in which I'm encoding the xml.