0

I'm trying to create my own SAML assertion generator that creates the same kind of assertion a connected app would when clicking the idP initiated login url because we need to automate that step for the users and there isn't any way to interact with connected apps through Apex that I can find. I've got my generator almost working but I believe I'm doing something wrong when creating the signature as the remote API is rejecting it saying the it doesn't match. Here is what I've got for the generator (I know it's kind of messy, there has been a lot of rapid dev done on this just to get PoC working).

public class SAMLServiceProviderController {


    private String issuer = 'urn:gen:abc:idp';
    private String audience = 'REMOVED';
    private String action = 'REMOVED';

    //PEM Encoded private key
    private String encodedKey = 'REMOVED';
    private string certificate = 'MIIDBjCCAe4CCQC7z5Z6YFX7aDANBgkqhkiG9w0BAQsFADBFMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMB4XDTE5MDMwNDE1MzAwNVoXDTE5MDQwMzE1MzAwNVowRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALzwIAcudWnz4uC3ZGRRLK+DuM7eSxbl8AG1XNt44WaX9mYeTjl9td34Q5H2P/aRppQ2jkq/tEHrplrEkIPhm8fb/pPKQe6UDiR6icX4WzTTBfVDPBgRTuNj/c9n1kp3eKvFNn53rxP9Vfx6PXtSuhhJWP+BXSSgvjRjmNgIrPpi6Tk5qLsRYhKdG661KrxLORcd6uiRKax8Rx1Eb1w7x0gonm4tZpjIKZQmqzlP5j3vxfWZvsOXNNU0QTZ9nv9VqmUKc6d/hCIcSyN8cUU5XNtXA3BDe/BiWbJTZqZhXTalmgfmEC6oS/JLdkAU8jlIjzH6YDUAfaCrKFWNjETsGSkCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEAIje/gKrSqbJQaCyJ1pu9mYpzUU5N4LtBgronnWbaPcvlxbQ3x49pPy+ds0/wd8gfzGaL7md+ssVpdH5oMH5UnAf00sUsIvRcSAvH0pu25CQVQW3Zifjc3tjElEEJ/iA1F/0Uue3kQQjEjE79yN1hK82jcEuugFKz53f3l65DxE7KAJrfOIAwDCYQD60dSwkdxYakn5mXWmtVbzcJG0hvHnv3+LUiwJEkJsKulWgXUiZ8Gw7+vRxd65VERW6/8cUh/9LY8OHFjbUWZZrMpP6kVi/uxxgTTaaB3TV0QaOlx9gnq7uMVGk9qweHwEQ1Zrxxd3U5/IP2crovnl29p+Batg==';

    public string getSaml()
    {
        //Set properties of the SAML attributes container
        SamlContainer samlAttributes = new SamlContainer();
        samlAttributes.ISSUER = issuer;
        samlAttributes.AUDIENCE = audience;
        samlAttributes.ACTION = action;
        samlAttributes.CERTIFICATE = certificate;
        samlAttributes.PRIVATE_KEY = encodedKey;
        samlAttributes.ACTION = action;
        SamlAttributes.RECIPIENT = audience;

        string preCannonicalizedResponse = getPreCannonicalizedResponse(samlAttributes);

        //Prepare the Digest
        Blob digest = Crypto.generateDigest('SHA1',  Blob.valueOf(preCannonicalizedResponse));
        String digestString = EncodingUtil.base64Encode(digest);

        //Prepare the SignedInfo
        string preCannonicalizedSignedInfo = getPreCannonicalizedSignedInfo();
        preCannonicalizedSignedInfo = preCannonicalizedSignedInfo.replaceAll('ASSERTION_ID',samlAttributes.ASSERTION_ID);
        preCannonicalizedSignedInfo = preCannonicalizedSignedInfo.replaceAll('DIGEST',digestString);
        preCannonicalizedSignedInfo = preCannonicalizedSignedInfo.replaceAll('CERTIFICATE_STRING',samlAttributes.CERTIFICATE);

        system.debug('\n\n\n-------- preCannonicalizedResponse string before any encryption with replacements');
        system.debug(preCannonicalizedSignedInfo);

        //Sign the SignedInfo
        Blob privateKey = EncodingUtil.base64Decode(SamlAttributes.PRIVATE_KEY);
        Blob input = Blob.valueOf(preCannonicalizedSignedInfo);

        Blob signature = Crypto.sign('RSA', input, privateKey);
        String signatureString = EncodingUtil.base64Encode(signature);

        //put the signature string into the XML (<ds:SignatureValue>)
        //preCannonicalizedSignedInfo = preCannonicalizedSignedInfo.replaceAll('SIGNATURE_STRING',signatureString);

        preCannonicalizedSignedInfo = preCannonicalizedSignedInfo.replaceAll('</ds:SignedInfo>','</ds:SignedInfo><ds:SignatureValue>'+signatureString+'</ds:SignatureValue>');
        //put our generated signature block into the payload (everything withing the <ds:Signature> tags)
        preCannonicalizedResponse = preCannonicalizedResponse.replaceAll('SIGNATURE_BLOCK', preCannonicalizedSignedInfo );

        //return the prefix + our prepped message
        System.debug('ASSERTION:' + preCannonicalizedResponse);
        return preCannonicalizedResponse;
    }

    public string doSAML(){
        String saml = getSaml();

        System.debug('\n===============SAML==============\n: ' + saml);
        http h = new http();
        httpRequest req = new httpRequest();
        req.setHeader('Content-Type','application/x-www-form-urlencoded');
        req.setMethod('POST');
        //system.debug(req.getBody());
        //String param = 'grant_type=' + encodingUtil.urlEncode('urn:ietf:params:oauth:grant-type:saml2-bearer','UTF-8')+'&assertion=' + base64URLencode(blob.valueOf(saml));
        string param = saml;
        param = EncodingUtil.base64Encode(blob.valueOf(saml));
        param = EncodingUtil.urlEncode(param,'UTF-8');
        req.setBody('SAMLResponse='+param);

        system.debug('\n\n\n------ Request Info');
        system.debug(req);
        system.debug(req.getBody());
        req.setEndpoint(action);
        httpResponse res = h.send(req);
        System.debug('\n\nRESPONSE: ' + res.getBody());
        System.debug('\n\nRESPONSE: ' + res.getStatusCode() + '  ' + res.getStatus());


        system.debug('\n\n\n---------- Custom Generated SAML Request Decoded');
        system.debug(param);
        string samlResponse1= param;
        samlResponse1 = EncodingUtil.urlDecode(samlResponse1,'UTF-8');
        system.debug('\n\n\n---------- URL Decoded');
        system.debug(samlResponse1);
        samlResponse1 = EncodingUtil.base64Decode(samlResponse1).toString();
        system.debug('\n\n\n---------- Final Decode');
        system.debug(samlResponse1);
        return res.getBody();
    }


    public string getPreCannonicalizedResponse(SamlContainer samlAttributes)
    {
        string infoString = '<samlp:Response xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" Destination="'+samlAttributes.ACTION+'" ID="'+samlAttributes.ASSERTION_ID+'" IssueInstant="'+samlAttributes.NOT_BEFORE+'" Version="2.0">'+
            '<saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'+samlAttributes.ISSUER+'</saml:Issuer>'+
            '<samlp:Status>'+
                '<samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />'+
            '</samlp:Status>'+
            '<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="'+samlAttributes.ASSERTION_ID+'" IssueInstant="'+samlAttributes.NOT_BEFORE+'" Version="2.0">'+
                '<saml:Issuer Format="urn:oasis:names:tc:SAML:2.0:nameid-format:entity">'+samlAttributes.ISSUER+'</saml:Issuer>'+
                'SIGNATURE_BLOCK'+
            '<saml:Subject>'+
                '<saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">'+samlAttributes.USER_ID+'</saml:NameID>'+
                '<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">'+
                    '<saml:SubjectConfirmationData NotOnOrAfter="'+samlAttributes.NOT_AFTER+'" Recipient="'+samlAttributes.ACTION+'" />'+
                '</saml:SubjectConfirmation>'+
            '</saml:Subject>'+
            '<saml:Conditions NotBefore="'+samlAttributes.NOT_BEFORE+'" NotOnOrAfter="'+samlAttributes.NOT_AFTER+'">'+
                '<saml:AudienceRestriction>'+
                    '<saml:Audience>'+samlAttributes.AUDIENCE+'</saml:Audience>'+
                '</saml:AudienceRestriction>'+
            '</saml:Conditions>'+
            '<saml:AuthnStatement AuthnInstant="'+samlAttributes.NOT_BEFORE+'">'+
                '<saml:AuthnContext>'+
                    '<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>'+
                '</saml:AuthnContext>'+
            '</saml:AuthnStatement>'+
            '<saml:AttributeStatement>'+
                '<saml:Attribute Name="userId" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">'+samlAttributes.USER_ID+'</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="username" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">'+samlAttributes.USER_NAME+'</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="email" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">'+samlAttributes.USER_EMAIL+'</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="is_portal_user" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">false</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="UserType" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                '   <saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">2</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="IdentityType" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">1</saml:AttributeValue>'+
                '</saml:Attribute>'+
                '<saml:Attribute Name="Target" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified">'+
                    '<saml:AttributeValue xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="xs:anyType">service_request</saml:AttributeValue>'+
                '</saml:Attribute>'+
            '</saml:AttributeStatement>'+
            '</saml:Assertion>'+
        '</samlp:Response>';

        return infoString;
    }
    public string getPreCannonicalizedSignedInfo()
    {
        string infoBlock = '<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/2000/09/xmldsig#rsa-sha1" />'+
            '<ds:Reference URI="#ASSERTION_ID">'+
                '<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#">'+
                        '<ec:InclusiveNamespaces xmlns:ec="http://www.w3.org/2001/10/xml-exc-c14n#" PrefixList="ds saml samlp xs xsi" />'+
                    '</ds:Transform>'+
                '</ds:Transforms>'+
                '<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />'+
                '<ds:DigestValue>DIGEST</ds:DigestValue>'+
            '</ds:Reference>'+
        '</ds:SignedInfo>'+
        '<ds:KeyInfo>'+
            '<ds:X509Data>'+
                '<ds:X509Certificate>CERTIFICATE_STRING</ds:X509Certificate>'+
            '</ds:X509Data>'+
            '</ds:KeyInfo>'+
        '</ds:Signature>';
        return infoBlock;
    }
    public class SamlContainer
    {
        public string ASSERTION_ID;
        public string NOT_ON_OR_AFTER;
        public string NOT_BEFORE;
        public string NOT_AFTER;
        public string AUDIENCE;
        public string RECIPIENT;
        public string ISSUER;
        public Id USER_ID;
        public string USER_NAME;
        public string USER_EMAIL;
        public string CERTIFICATE;
        public string SIGNATURE_VALUE;
        public string SIGNATURE_STRING;
        public string DIGEST;
        public string PRIVATE_KEY;
        public string ACTION;

        public SamlContainer()
        {
            datetime nowDt = datetime.now();
            Long nowLong = nowDt.getTime();
            datetime notBeforeDt = datetime.newInstance(nowLong - 120000);

            string notBefore = notBeforeDt.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss') + 'Z';
            datetime notOnOrAfterDt = nowDt.addMinutes(5);
            string notOnOrAfter = notOnOrAfterDt.formatGmt('yyyy-MM-dd\'T\'HH:mm:ss') + 'Z';

            Double random = Math.random();

            this.ASSERTION_ID = EncodingUtil.convertToHex(Crypto.generateDigest('SHA256',  Blob.valueOf('assertion' + random)));
            this.NOT_ON_OR_AFTER = notOnOrAfter;
            this.NOT_BEFORE = notBefore;
            this.NOT_AFTER = notOnOrAfter;
            this.USER_ID = userinfo.getUserId();
            this.USER_EMAIL = userInfo.getUserEmail();
            this.USER_NAME = userInfo.getUserName();
        }
    }
}

I assume I'm either feeding too much, or too little of the assertion data into the signature but I'm not sure exactly what needs to change. I can post the XML it's generating if that is helpful as well (omitted due to post length limits).

Any help is much appreciated. Thank you.

  • What exactly is your use case? – Aayush K Mar 4 at 22:15
  • I need to generate a SAML assertion to send to a remote system that uses it for authentications and then returns to me a token I can use to further access their API. – Kenji776 Mar 5 at 0:54
  • Once you fill out the form on this page and click the Request button, it displays the corresponding SAML assertion. You can compare your output with this to see if that helps. – Aayush K Mar 5 at 15:49
  • Is there a reason you can't use out of the box facility (connected app) for generating SAML? – identigral Mar 12 at 0:55
  • 1
    Yes, as the service we are calling returns a token in response to the callout that we need to record and use for additional transactions. As far as I am aware Connected Apps does not have a capability to capture any responses. Additionally this transaction needs to happen as part of an Apex transaction as it is actually just the first step in getting a custom lightning component to fetch data from the remote API. – Kenji776 Mar 13 at 15:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.