2

I'm trying to follow this guide: SAML Bearer Flow

I have the following PHP to build the assertion and post it:

//header('Content-type: text/xml');
$assertion = createAssertion("consumer_key_from_salesforce_connected_app", "https://test.salesforce.com", "https://test.salesforce.com/services/oauth2/token", "username@company.com.test");
//echo $assertion;
//exit();
$result = postAssertionToEndpoint("https://xxxxxxxxxxxx.cs18.my.salesforce.com/services/oauth2/token?so=00Dxxxxxxxxx", $assertion);

exit();

function postAssertionToEndpoint($url, $assertion) {
    //set POST variables
    $fields = array(
        'Content-Type' => 'application/x-www-form-urlencoded',
        'grant_type' => urlencode("urn:ietf:params:oauth:grant-type:saml2-bearer"),
        'assertion' => urlencode(base64_encode($assertion))
    );

    //url-ify the data for the POST
    foreach ($fields as $key => $value) {
        $fields_string .= $key . '=' . $value . '&';
    }
    rtrim($fields_string, '&');

    //open connection
    $ch = curl_init();

    //set the url, number of POST vars, POST data
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_POST, count($fields));
    curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
    curl_setopt($ch, CURLOPT_VERBOSE, 1);
    curl_setopt($ch, CURLOPT_HEADER, 1);
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
    //echo $fields_string;
    //execute post
    $response = curl_exec($ch);

    $err = curl_errno($ch);
    $errmsg = curl_error($ch);
    $header = curl_getinfo($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);

    $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    $header = substr($response, 0, $header_size);
    $body = substr($response, $header_size);

    //close connection
    curl_close($ch);

    return $response;
}

function createAssertion($issuer, $audience, $recipient, $subject) {
    $date = new DateTime();
    $theTime = $date->format("Y-m-d\TH:i:s.u");

    $date->add(new DateInterval('PT3600S'));
    $timeWindow = $date->format("Y-m-d\TH:i:s.u");

    $pathSampleAssertion = "resource/sampleAssertion.xml";

    try {
        if (file_exists($pathSampleAssertion)) {

            $assertion = file_get_contents($pathSampleAssertion, true);

            $xmlDoc = new DOMDocument();

            $xmlDoc->load($pathSampleAssertion);
            $xmlDoc->formatOutput = true;
            $xpath = new DOMXPath($xmlDoc);

            $nodeIssuer = $xpath->query('/saml:Assertion/saml:Issuer');

            foreach ($nodeIssuer as $node) {
                $node->nodeValue = $issuer;
            }

            $nodeAudience = $xpath->query('/saml:Assertion/saml:Conditions/saml:AudienceRestriction/saml:Audience');
            foreach ($nodeAudience as $node) {
                $node->nodeValue = $audience;
            }

            $nodeRecipient = $xpath->query('/saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData');
            foreach ($nodeRecipient as $node) {
                $node->setAttribute("Recipient", $recipient);
            }

            $nodeSubject = $xpath->query('/saml:Assertion/saml:Subject/saml:NameID');
            foreach ($nodeSubject as $node) {
                $node->nodeValue = $subject;
            }

            $nodes = $xpath->query('/saml:Assertion/saml:Subject/saml:SubjectConfirmation/saml:SubjectConfirmationData');
            foreach ($nodes as $node) {
                $node->setAttribute("NotOnOrAfter", $theTime);
            }
            $nodes = $xpath->query('/saml:Assertion/saml:Conditions');
            foreach ($nodes as $node) {
                $node->setAttribute("NotBefore", $theTime);
                $node->setAttribute("NotOnOrAfter", $timeWindow);
            }
            $nodes = $xpath->query('/saml:Assertion/saml:AuthnStatement ');
            foreach ($nodes as $node) {
                $node->setAttribute("AuthnInstant", $theTime);
            }

            $nodeAssertion = $xpath->query('/saml:Assertion');

            if (is_null($nodeAssertion)) {
                throw new Exception("couldn't get assertion node", 1, 1);
            }
            foreach ($nodeAssertion as $node) {
                $assertion = $node;
                $node->setAttribute("IssueInstant", $theTime);
            }

            $private_key_contents = <<<"PRIVKEY"
-----BEGIN RSA PRIVATE KEY-----
contents of private key generated with certificate attached to
connected app
...
G9pYntcKde9qTgB8j3Osrx3R9Mn4ffEtiQtDB45UFIw=
-----END RSA PRIVATE KEY-----
PRIVKEY;

            $pem = <<<"PEM"
-----BEGIN CERTIFICATE-----
contents of cert uploaded to salesforce connected app
...
Jq4JWU6FdIx03p6RWK6WS7JJcnYH7DXP4Ive3+Y=
-----END CERTIFICATE-----
PEM;

            //Private KEY   
            $objKey = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private'));
            //$private_key = "resource/privkey.pem";
            //$objKey->loadKey(file_get_contents($private_key));
            $objKey->loadKey($private_key_contents);

            //Sign the Assertion
            $objXMLSecDSig = new XMLSecurityDSig();
            $objXMLSecDSig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N);

            $objXMLSecDSig->addReferenceList(array($assertion), XMLSecurityDSig::SHA256, array('http://www.w3.org/2000/09/xmldsig#enveloped-signature', XMLSecurityDSig::EXC_C14N), array('id_name' => 'ID'));

            $objXMLSecDSig->sign($objKey);
            $objXMLSecDSig->add509Cert($pem);


            $objXMLSecDSig->appendSignature($assertion, false);

            return $xmlDoc->saveXML();
        }
    } catch (Exception $x) {
        echo "Problem: " . $x->getMessage();
    }
}
?>

this generates the following XML

<?xml version="1.0" encoding="UTF-8"?>
<saml:Assertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" ID="pfx1be36a2e-53f2-9f19-d306-d920520d70a6" IssueInstant="2014-11-18T09:19:25.000000" 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">...</saml:Issuer>
    <saml:Subject xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
        <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">username@example.com.test</saml:NameID>
        <saml:SubjectConfirmation xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
            <saml:SubjectConfirmationData NotOnOrAfter="2014-11-18T09:19:25.000000" Recipient="https://test.salesforce.com/services/oauth2/token"/>
        </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" NotBefore="2014-11-18T09:19:25.000000" NotOnOrAfter="2014-11-18T10:19:25.000000">
        <saml:AudienceRestriction xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
            <saml:Audience>https://test.salesforce.com</saml:Audience>
        </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion" AuthnInstant="2014-11-18T09:19:25.000000">
        <saml:AuthnContext xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
            <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:unspecified</saml:AuthnContextClassRef>
        </saml:AuthnContext>
    </saml:AuthnStatement>
    <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="#pfx1be36a2e-53f2-9f19-d306-d920520d70a6">
                <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>7h6TEjVC+5fOssAd5Ore6fZ/eyZ2xNIbdBVWw0vzeQo=</ds:DigestValue>
            </ds:Reference>
        </ds:SignedInfo>
        <ds:SignatureValue>...</ds:SignatureValue>
        <ds:KeyInfo>
            <ds:X509Data>
                <ds:X509Certificate>...</ds:X509Certificate>
            </ds:X509Data>
        </ds:KeyInfo>
    </ds:Signature>
</saml:Assertion>

when posted, I get the response:

HTTP/1.1 100 Continue

HTTP/1.1 400 Bad Request
Date: Tue, 18 Nov 2014 08:37:16 GMT
Set-Cookie: BrowserId=SmGfiJYHTjKctXy2WefrOQ;Path=/;Domain=.salesforce.com;Expires=Sat, 17-Jan-2015 08:37:16 GMT
Expires: Thu, 01 Jan 1970 00:00:00 GMT
Pragma: no-cache
Cache-Control: no-cache, no-store
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked

{"error_description":"invalid assertion","error":"invalid_grant"}

I've setup the connected app to allow all scopes Admin approved users are pre-authorized I've added a permission set to the connected app and assigned it to my user and set Relax IP restrictions

At this stage, I don't know if:

  • The assertion I generated is invalid
  • The salesforce setup is invalid
  • I'm posting it incorrectly
3

Content-Type is an HTTP header, not a form field, so it needs to be passed thus:

curl_setopt($ch, CURLOPT_HTTPHEADER, array(
    'Content-Type' => 'application/x-www-form-urlencoded'
));

Also, the assertion must be base64url encoded (not just base 64 encoded). Add a base64url_encode function like:

function base64url_encode($data) { 
    return rtrim(strtr(base64_encode($data), '+/', '-_'), '=');
}

And modify your fields array:

//set POST variables
$fields = array(
    'grant_type' => urlencode("urn:ietf:params:oauth:grant-type:saml2-bearer"),
    'assertion' => urlencode(base64url_encode($assertion))
);

Finally, the assertion signature should be after the Issuer and before the Subject - see section 2.3.3 of the SAML 2.0 Core Spec for the schema, and Example SAML Assertions.

You might also find SimpleSAMLphp useful. Not sure if you can use the SAML Assertion generator for your purposes, but you can certainly take a look at the open source.

  • thanks metadaddy, this looks like it might sort my problem. I've still not got it working, as I'm having trouble getting the signature between Issuer and Subject. I'm new to php, saml and salesforce so it might take me a while to get this sorted. everything else in your answer makes sense and it's definitely moved me forward a bit! Still not sure I'm taking the right approach... I basically want to get the bearer token so I can consume the salesforce Chatter rest api from a php web app without the end user having to authorise. (Since they won't know that it is salesforce under the hood!) – kingmunkyman Dec 3 '14 at 20:14

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.