Skip to content

Commit

Permalink
EAC-34165 Entra compatability for SLO (#6)
Browse files Browse the repository at this point in the history
  • Loading branch information
chet-wynn-cookieai authored Apr 22, 2024
1 parent 87df125 commit 2a7d918
Show file tree
Hide file tree
Showing 3 changed files with 186 additions and 6 deletions.
41 changes: 35 additions & 6 deletions service_provider_signed.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"errors"
"fmt"
"net/url"
"regexp"
"strings"

dsig "github.com/russellhaering/goxmldsig"
)
Expand All @@ -28,16 +30,37 @@ var (
// https://github.com/grafana/saml/blob/a6c0e9b86a4c064fa5a593a0575d8656d533e13e/service_provider_signed.go
func (sp *ServiceProvider) validateQuerySig(query url.Values) error {
sig := query.Get("Signature")
alg := query.Get("SigAlg")
if sig == "" || alg == "" {
if sig == "" {
return ErrNoQuerySignature
}

// Signature is base64 encoded
sigBytes, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
return fmt.Errorf("failed to decode signature: %w", err)
}

certs, err := sp.getIDPSigningCerts()
if err != nil {
return err
}

// standard url encoding
if err := sp.validateQuerySigVariant(query, sigBytes, certs, false); err == nil {
return nil
}
// entra/azure, which requires lowercase url encoding
return sp.validateQuerySigVariant(query, sigBytes, certs, true)
}

// validateQuerySigVariant validates of the signature of the Redirect Binding in query values; supports lowering URL
// encoding for Entra compatability
func (sp *ServiceProvider) validateQuerySigVariant(query url.Values, sigBytes []byte, certs []*x509.Certificate, toLowercase bool) error {
alg := query.Get("SigAlg")
if alg == "" {
return ErrNoQuerySignature
}

respType := ""
switch {
case query.Get("SAMLResponse") != "":
Expand All @@ -60,10 +83,16 @@ func (sp *ServiceProvider) validateQuerySig(query url.Values) error {

res += "&SigAlg=" + url.QueryEscape(alg)

// Signature is base64 encoded
sigBytes, err := base64.StdEncoding.DecodeString(sig)
if err != nil {
return fmt.Errorf("failed to decode signature: %w", err)
// This lowers URL encoding to be compatible with Entra/AzureAD signatures
// See https://github.com/SAML-Toolkits/python3-saml/blob/master/src/onelogin/saml2/utils.py#L72
if toLowercase {
re, err := regexp.Compile("%[A-F0-9]{2}")
if err != nil {
return err
}
res = re.ReplaceAllStringFunc(res, func(s string) string {
return strings.ToLower(s)
})
}

var (
Expand Down
52 changes: 52 additions & 0 deletions service_provider_signed_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ package saml

import (
"crypto/rsa"
"crypto/x509"
"encoding/base64"
"encoding/xml"
"net/url"
"regexp"
"testing"

dsig "github.com/russellhaering/goxmldsig"
Expand Down Expand Up @@ -89,6 +91,56 @@ func TestSigningAndValidation(t *testing.T) {
}
}

// TestValidation_AzureEntra compatability test with Azure, which requires url encoding to be lowercased
func TestValidation_AzureEntra(t *testing.T) {
certStr := "MIIC8DCCAdigAwIBAgIQXzpLPP73pKBCobXFPkIGbDANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQD\nEylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yNDA0MTYxMzM5\nNTBaFw0yNzA0MTYxMzM5NTBaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQg\nU1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuXwYN9Nq6tzq\n3KmGE6Wb7gvR99ezuCCjqd0VljFtt1B57yiQf7o9JLqGWhRgSqlLgctKdqyISYCr4KsFQOwKDow+\nu/2sJe4129xlI4f1vXC+uGByKvFwn4tRpIyhmYjRT4pnTSbLEJ4y2i34ZhUiic1s057AY78H5gX7\nwCAS9EzWN5GE5vzSaQBlhjH8c7lfMi7NPjh3Y1QwEYhQfgGZ8cpceppYz4uaJ0JqOhz+NzHi7OBd\n+Srw8LmgVvaZcoC+CAVDkNCJejfwckTz8Jo5ZK5ngih3ecXkfjoUs9sSArrd7O90EmWj+rx6NFwn\nZ5SRLxg/Ek0hDeLL9r/zXGLk1QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQApGAHsj1+9fV2niS2V\nMhd3IxBbogu/RQ/3eKuZLmmmQFtp/cBxLIT1wNazh5mXMvd4CYITdDJSmDzxdbBOApxwk7VdudQL\n0VTzYO9NBrt88Lvmat+7L7M0QRw1y/iYF6oZLLNw6bkY0SwHgmoNQQVnup7kJT54/LzZJ8Fhh8mc\nUc/uLzlTuWY7plmVSM7dicMhcYHGiSn2BPet9Infl0DV2O728G5cosVs0bTFX6s5g24H2ysbQSHF\na3OuYpHdVZTX7fDlYC4otqC+JI1Y2x1PPx7b9wK2ezDl5u3kd+r9QViFXo6vxrVpv3Za9zl1oP8M\nYeO8oWPlmQrEpPq2usJ8\n"
requestType := samlResponse
request := "fZJBT8MwDIX%2fSpV7mmRNsyTqKiG4TIILQxy4IKd1WUWXTHUq%2bPmUTRyQEEdbfn7vk90QnKazv09vacmPSOcUCYv93Y69oq3AuNDxTZCBaxmQ21ojd1ZXdQjaDDaw4hlnGlPcsU0pWbEnWnAfKUPMa0tuNJeaK%2fuknFdbX6tSW%2fnCijukPEbIF%2bUx5zN5ITLGVabKLqX3EWEs8zrlrbRKwJKP4juqoCmtNvEn6lPasbHnIDfg7FaroXYhGNRKaWOdqrZYmd51sjMQ5OBY8XmaIvkL9Y4tc%2fQJaCQf4YTkc%2bcPNw%2f3fmXx5znl1KWJtc2Far5K%2fxcBEc7fVKz9oaJM5ccY%2b%2fRBZcQswFjTD9ueo6st11gPPGhruTE16MqCAtmLRlw92%2bZ6n0OGvNDv6jb1WDzDtOD%2fmegy7Q9L1yERE20jfi8Vf%2f1A%2bwU%3d"
signature := "lInEk5gHMixezv6xlcIBcScceeTzoI9qrw0Oeho8ARlTFjsiuk3J%2fODzv3bo526Dl0rrqIBLC%2bmnRHWoy5hSbKHnpy6sR3WEQu6RRVp4d1Uf6kgGXfiZDM9eXMwjSYoTN3OhX3vNzlF2iXY8cWy06xIH3V9EwE8K4DYeheUP0%2b8jGRkvyWlKqKTLQSSem%2bHp9LkuH8LcODXPleWTJD1TXpiYHbCszJxdTE2ueNW2UNiLB4M4zFSNahA%2bpXRWkyEnHMAxozq5ugpvDVomqX%2f9Gjc9135gzEVz%2f04EWtXLgBEa%2fPUKObheQpLEufqfeFUhGEQRlpHQ3u72CyvbxWAhxg%3d%3d"
algorithm := "http%3a%2f%2fwww.w3.org%2f2001%2f04%2fxmldsig-more%23rsa-sha256"

// parse pem certificate
regex := regexp.MustCompile(`\s+`)
certStr = regex.ReplaceAllString(certStr, "")
certBytes, err := base64.StdEncoding.DecodeString(certStr)
if err != nil {
panic(err)
}
cert, err := x509.ParseCertificate(certBytes)
if err != nil {
panic(err)
}

idpMetadata := golden.Get(t, "SP_IDPMetadata_signing_entra")
s := ServiceProvider{
Key: mustParsePrivateKey(golden.Get(t, "idp_key.pem")).(*rsa.PrivateKey), // placeholder; not used
Certificate: cert,
MetadataURL: mustParseURL("https://15661444.ngrok.io/saml2/metadata"),
AcsURL: mustParseURL("https://15661444.ngrok.io/saml2/acs"),
SignatureMethod: dsig.RSASHA256SignatureMethod,
}

err = xml.Unmarshal(idpMetadata, &s.IDPMetadata)
assert.NilError(t, err)

idpCert, err := s.getIDPSigningCerts()

assert.Check(t, err == nil)
assert.Check(t,
s.Certificate.Issuer.CommonName == idpCert[0].Issuer.CommonName, "expected %s, got %s",
s.Certificate.Issuer.CommonName, idpCert[0].Issuer.CommonName)

rawQuery := string(requestType) + "=" + request
rawQuery += "&Signature=" + signature
rawQuery += "&SigAlg=" + algorithm

query, err := url.ParseQuery(rawQuery)
assert.NilError(t, err, "error parsing query: %s", err)

err = s.validateQuerySig(query)
assert.NilError(t, err, "error validating query: %s", err)
}

// Given a raw query with an unsupported signature method, the signature should be rejected.
func TestInvalidSignatureAlgorithm(t *testing.T) {
rawQuery := "SAMLRequest=PHNhbWxwOkF1dGhuUmVxdWVzdCB4bWxuczpzYW1sPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIiB4bWxuczpzYW1scD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiBJRD0iaWQtMDAwMjA0MDYwODBhMGMwZTEwMTIxNDE2MTgxYTFjMWUyMDIyMjQyNiIgVmVyc2lvbj0iMi4wIiBJc3N1ZUluc3RhbnQ9IjIwMTUtMTItMDFUMDE6NTc6MDlaIiBEZXN0aW5hdGlvbj0iaHR0cHM6Ly9pZHAuZXhhbXBsZS5jb20vc2FtbC9zc28iIEFzc2VydGlvbkNvbnN1bWVyU2VydmljZVVSTD0iaHR0cHM6Ly9zcC5leGFtcGxlLmNvbS9zYW1sMi9hY3MiIFByb3RvY29sQmluZGluZz0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmJpbmRpbmdzOkhUVFAtUE9TVCI%2BPHNhbWw6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5Ij5odHRwczovL3NwLmV4YW1wbGUuY29tL3NhbWwyL21ldGFkYXRhPC9zYW1sOklzc3Vlcj48c2FtbHA6TmFtZUlEUG9saWN5IEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6dHJhbnNpZW50IiBBbGxvd0NyZWF0ZT0idHJ1ZSIvPjwvc2FtbHA6QXV0aG5SZXF1ZXN0Pg%3D%3D&RelayState=AAAAAAAAAAAA&SigAlg=http%3A%2F%2Fwww.w3.org%2F2000%2F09%2Fxmldsig%23rsa-sha384&Signature=zWAF4S%2FIs7tfmEriOsT5Fm8EFOGS3iCq6OxP5i7hM%2BMPwAoXwdDz6fKH8euS1gQ3sGOZBdHD588FZLvnO1OeCxLaEsxHMVKsAZSZFLBmPPwqB6e%2B84cCwX2szOeoMROaR%2B36mdoBDRQz36JIvyBBG%2FND9x41k%2FGQuAuwk%2B9fkuE%3D"
Expand Down
99 changes: 99 additions & 0 deletions testdata/SP_IDPMetadata_signing_entra
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<EntityDescriptor xmlns="urn:oasis:names:tc:SAML:2.0:metadata" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:mdalg="urn:oasis:names:tc:SAML:metadata:algsupport" xmlns:mdui="urn:oasis:names:tc:SAML:metadata:ui" xmlns:shibmd="urn:mace:shibboleth:metadata:1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" Name="urn:mace:shibboleth:testshib:two" entityID="https://idp.testshib.org/idp/shibboleth">
<Extensions>
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha512" />
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#sha384" />
<mdalg:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256" />
<mdalg:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha512" />
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha384" />
<mdalg:SigningMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256" />
<mdalg:SigningMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
</Extensions>
<IDPSSODescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:mace:shibboleth:1.0 urn:oasis:names:tc:SAML:2.0:protocol">
<Extensions>
<shibmd:Scope regexp="false">testshib.org</shibmd:Scope>
<mdui:UIInfo>
<mdui:DisplayName xml:lang="en">TestShib Test IdP</mdui:DisplayName>
<mdui:Description xml:lang="en">TestShib IdP. Use this as a source of attributes
for your test SP.</mdui:Description>
<mdui:Logo height="88" width="253">https://www.testshib.org/testshibtwo.jpg</mdui:Logo>
</mdui:UIInfo>
</Extensions>
<KeyDescriptor>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIC8DCCAdigAwIBAgIQXzpLPP73pKBCobXFPkIGbDANBgkqhkiG9w0BAQsFADA0MTIwMAYDVQQD
EylNaWNyb3NvZnQgQXp1cmUgRmVkZXJhdGVkIFNTTyBDZXJ0aWZpY2F0ZTAeFw0yNDA0MTYxMzM5
NTBaFw0yNzA0MTYxMzM5NTBaMDQxMjAwBgNVBAMTKU1pY3Jvc29mdCBBenVyZSBGZWRlcmF0ZWQg
U1NPIENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuXwYN9Nq6tzq
3KmGE6Wb7gvR99ezuCCjqd0VljFtt1B57yiQf7o9JLqGWhRgSqlLgctKdqyISYCr4KsFQOwKDow+
u/2sJe4129xlI4f1vXC+uGByKvFwn4tRpIyhmYjRT4pnTSbLEJ4y2i34ZhUiic1s057AY78H5gX7
wCAS9EzWN5GE5vzSaQBlhjH8c7lfMi7NPjh3Y1QwEYhQfgGZ8cpceppYz4uaJ0JqOhz+NzHi7OBd
+Srw8LmgVvaZcoC+CAVDkNCJejfwckTz8Jo5ZK5ngih3ecXkfjoUs9sSArrd7O90EmWj+rx6NFwn
Z5SRLxg/Ek0hDeLL9r/zXGLk1QIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQApGAHsj1+9fV2niS2V
Mhd3IxBbogu/RQ/3eKuZLmmmQFtp/cBxLIT1wNazh5mXMvd4CYITdDJSmDzxdbBOApxwk7VdudQL
0VTzYO9NBrt88Lvmat+7L7M0QRw1y/iYF6oZLLNw6bkY0SwHgmoNQQVnup7kJT54/LzZJ8Fhh8mc
Uc/uLzlTuWY7plmVSM7dicMhcYHGiSn2BPet9Infl0DV2O728G5cosVs0bTFX6s5g24H2ysbQSHF
a3OuYpHdVZTX7fDlYC4otqC+JI1Y2x1PPx7b9wK2ezDl5u3kd+r9QViFXo6vxrVpv3Za9zl1oP8M
YeO8oWPlmQrEpPq2usJ8</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
</KeyDescriptor>
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://idp.testshib.org:8443/idp/profile/SAML1/SOAP/ArtifactResolution" index="1" />
<ArtifactResolutionService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.testshib.org:8443/idp/profile/SAML2/SOAP/ArtifactResolution" index="2" />
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.testshib.org/idp/profile/SAML2/POST/SLO" />
<SingleLogoutService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.testshib.org/idp/profile/SAML2/Redirect/SLO" />
<SingleSignOnService Binding="urn:mace:shibboleth:1.0:profiles:AuthnRequest" Location="https://idp.testshib.org/idp/profile/Shibboleth/SSO" />
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" Location="https://idp.testshib.org/idp/profile/SAML2/POST/SSO" />
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect" Location="https://idp.testshib.org/idp/profile/SAML2/Redirect/SSO" />
<SingleSignOnService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.testshib.org/idp/profile/SAML2/SOAP/ECP" />
</IDPSSODescriptor>
<AttributeAuthorityDescriptor protocolSupportEnumeration="urn:oasis:names:tc:SAML:1.1:protocol urn:oasis:names:tc:SAML:2.0:protocol">
<KeyDescriptor>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>MIIB7zCCAVgCCQDFzbKIp7b3MTANBgkqhkiG9w0BAQUFADA8MQswCQYDVQQGEwJV
UzELMAkGA1UECAwCR0ExDDAKBgNVBAoMA2ZvbzESMBAGA1UEAwwJbG9jYWxob3N0
MB4XDTEzMTAwMjAwMDg1MVoXDTE0MTAwMjAwMDg1MVowPDELMAkGA1UEBhMCVVMx
CzAJBgNVBAgMAkdBMQwwCgYDVQQKDANmb28xEjAQBgNVBAMMCWxvY2FsaG9zdDCB
nzANBgkqhkiG9w0BAQEFAAOBjQAwgYkCgYEA1PMHYmhZj308kWLhZVT4vOulqx/9
ibm5B86fPWwUKKQ2i12MYtz07tzukPymisTDhQaqyJ8Kqb/6JjhmeMnEOdTvSPmH
O8m1ZVveJU6NoKRn/mP/BD7FW52WhbrUXLSeHVSKfWkNk6S4hk9MV9TswTvyRIKv
Rsw0X/gfnqkroJcCAwEAATANBgkqhkiG9w0BAQUFAAOBgQCMMlIO+GNcGekevKgk
akpMdAqJfs24maGb90DvTLbRZRD7Xvn1MnVBBS9hzlXiFLYOInXACMW5gcoRFfeT
QLSouMM8o57h0uKjfTmuoWHLQLi6hnF+cvCsEFiJZ4AbF+DgmO6TarJ8O05t8zvn
OwJlNCASPZRH/JmF8tX0hoHuAQ==</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes192-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#tripledes-cbc" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p" />
<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
</KeyDescriptor>
<AttributeService Binding="urn:oasis:names:tc:SAML:1.0:bindings:SOAP-binding" Location="https://idp.testshib.org:8443/idp/profile/SAML1/SOAP/AttributeQuery" />
<AttributeService Binding="urn:oasis:names:tc:SAML:2.0:bindings:SOAP" Location="https://idp.testshib.org:8443/idp/profile/SAML2/SOAP/AttributeQuery" />
<NameIDFormat>urn:mace:shibboleth:1.0:nameIdentifier</NameIDFormat>
<NameIDFormat>urn:oasis:names:tc:SAML:2.0:nameid-format:transient</NameIDFormat>
</AttributeAuthorityDescriptor>
<Organization>
<OrganizationName xml:lang="en">TestShib Two Identity Provider</OrganizationName>
<OrganizationDisplayName xml:lang="en">TestShib Two</OrganizationDisplayName>
<OrganizationURL xml:lang="en">http://www.testshib.org/testshib-two/</OrganizationURL>
</Organization>
<ContactPerson contactType="technical">
<GivenName>Nate</GivenName>
<SurName>Klingenstein</SurName>
<EmailAddress>[email protected]</EmailAddress>
</ContactPerson>
</EntityDescriptor>

0 comments on commit 2a7d918

Please sign in to comment.