Monday, September 11, 2017

ADFS and the SAML2 Artifact Binding woes

We've been using the WS-Fed for ages, I myself wrote a couple of tutorials and was often emailed by people asking for some details. As time passes, the Ws-Fed is pushed away in favor of the SAML2 protocol. And yes, SAML2 has some significant advantages over Ws-Fed, namely it supports multiple bindings.

The idea behind bindings is to allow both the token request and the actual token response be transmitted in various ways. Specifically, if you have ever worked with the WS-Fed, you are aware that the request from the Relying Party to the Identity Provider is passed over the browser's query string

https://idp.com?wa=wsignin1.0&wtrealm=https://rp.com
and the response is always passed in a HTML form that is POSTed to the Relying Party. On the other hand, the OAuth2/OpenID Connect have a different way of passing messages - while the request is also passed over the browser's query string
https://idp.com?wa=response_type=code&client_id=foobar&redirect_uri=https://rp.com&scope=profile
it is the RP that creates a direct HTTPs request to the IdP's profile endpoint to retrieve the user token.

This subtle difference has substantial consequences. In particular:

  • since the WS-Fed SAML1.1 token is passed from the IdP to the RP using the client's browser, it has to be signed to prevent it from being tampered. Also, large tokens could possibly introduce unwanted lag issues as it unfortunately takes some time to POST large data from a browser to a webserver
  • the OAuth2 tokens don't have to be signed as the trust is established by direct requests from the RP to the IdP, which, on the other hand could possibly be one of attack vectors (suppose the DNS can be poisoned at the RP's side and direct requests from the RP to the IdP target an attacker's site instead of the IdP

These two examples show that there are three different ways of passing the data between the RP and the IdP and this is exactly what three most important SAML2 binding are about

  1. the HTTP-Redirect binding consists in a 302 redirect and passing data in the query string
  2. the HTTP-POST binding consists of a HTTP POSTed form where the data is included in the form's inputs
  3. the Artifact binding consists in a redirect (302 or POST) where only a single artifact is returned, the receiver creates a HTTPs request passing the artifact as an argument and gets the data as the response
Note that any binding can be used to initiate the flow from the RP to the IdP and any other binding can be used to pass the SAML2 response token from the IdP to the RP.

For example, the RP can use the HTTP-Redirect binding to initiate the flow and then the IdP can use the HTTP-POST binding to send the token back (this particular combination of bindings would make such SAML2 negotiation very similar to WS-Fed):

Or, the RP can use the HTTP-Redirect binding to send the request to the IdP but can get the response back with the Artifact binding (which would make it look similar to OAuth2):

What this blog entry is about is how difficult it was to implement the SAML2's Artifact binding in a scenario where the ADFS is the actual Identity Provider.

First, why would you implement the SAML2 for yourself? Well, few reasons:

  • practical - the SAML2 is not supported by the base class library, although SAML1.1 (Ws-Fed) is. There are some classes in the base class library (the SAML2 token, the token handler) but the SAML2 protocol classes (handling different bindings) are missing. It's just like somebody has started the work but was interrupted in the middle of it and was never allowed to return and finish the job. There are some free and paid implementations (look here, or here, or here and google paid libraries on your own)
  • practical - although some SAML2 implementations can be found, it looks like this particular binding, the Artifact binding is not always supported, even though the other two bindings usually are supported
  • theoretical - to better understand all the details and have a full control over what happends under the hood of the Artifact binding

The implementation turned out to be more or less straightforward as long as HTTP-Redirect and HTTP-POST bindings are considered. It turned out, however, that the Artifact binding is a malicious strain and just doesn't want to cooperate that easily. Problems started when the ADFS was expected to return the artifact that the Artifact Resolve endpoint at the ADFS's side was about to be queried so the artifact could be exchanged for a SAML2 token. A following list enumerates some possible issues and could be used as a checklist not only by implementors but also by administrators who configure a channel between a Relying Party and the ADFS based on the Artifact binding.

  1. make sure you read the SAML 2.0 ADFS guide from Microsoft
  2. make sure the Artifact Resolve endpoint is turned on in the ADFS configuration
  3. make sure the signing certificate is imported in the Relying Party Trust Properties on the Signing tab - the ArtifactResolve request has to be signed by the RP's certificate, otherwise ADFS will reject the request (actually, ADFS tends to return an empty ArtifactResponse, with StatusCode equal to urn:oasis:names:tc:SAML:2.0:status:Success but no token included
  4. make sure you set correct signature algorithm in the Properties on the Advanced tab - by default ADFS expects the ArtifactResolve will be signed using SHA256RSA, however, your certificate can be SHA1RSA one
  5. make sure you consult the Event Log entries (under Applications and Services Logs/AD FS 2.0/Admin) to find detailed information on request processing. What you will get at the RP's side from sending invalid request is just urn:oasis:names:tc:SAML:2.0:status:Requester (which means that your request is incorrect for some reason) or urn:oasis:names:tc:SAML:2.0:status:Responder (which means ADFS is misconfigured). On the other hand, peeking into the system log can sometimes reveal additional details, e.g.
  6. make sure you sign the ArtifactResolve request and then you put it into a SOAP Envelope. Make sure you don't sign the whole envelope. This is how a correct request should look like
    <s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">
      <s:Body>
        <samlp:ArtifactResolve
          xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
          xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"
          ID="_cce4ee769ed970b501d680f697989d14"
          Version="2.0"
          IssueInstant="current-date-here e.g. 2017-09-11T12:00:00">
        <saml:Issuer>the ID of the RP from ADFS config e.g. https://rp.com</saml:Issuer>
        <ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">...</ds:Signature>
        <samlp:Artifact>the artifact goes here</samlp:Artifact>
      </samlp:ArtifactResolve>    
      </s:Body>
    </s:Envelope>
    
  7. (this one was tricky)make sure the Signature goes between the Issuer and Artifact nodes. By taking a look into ADFS sources (the Microsoft.IdentityServer.dll) you could learn that ADFS has its own exclusive, hand-written algorithm of signature validation which doesn't use the .NET's SignedXml class at all. This manually coded algorithm expects this particular order of nodes and logs 'Element' is an invalid XmlNodeType (as shown at the screenshot above, not too helpful) if you put your signature as the very last node. Frankly, I don't see any sense in this limitation at all.
  8. make sure you set SignedInfo's CanonicalizationMethod to SignedXml.XmlDsigExcC14NTransformUrl (which resolves to http://www.w3.org/2001/10/xml-exc-c14n#) (for some reason the ADFS just doesn't accept the default SignedXml.XmlDsigC14NTransformUrl aka http://www.w3.org/TR/2001/REC-xml-c14n-20010315)
  9. (this was even tricker) make sure you add XmlDsigExcC14NTransform to the Reference element of the signature. The relevant part of the ArtifactResolve code is presented below. It expects the ArtifactResolve XML in a string form, the signing certificate and the id of the ArtifactResolve (to put it into the signature)
            public string SignArtifactResolve(string artifactResolveBody, X509Certificate2 certificate, string id)
            {
                if (artifactResolveBody == null || certificate == null || string.IsNullOrEmpty( id ))
                    throw new ArgumentNullException();
    
                XmlDocument doc = new XmlDocument();
                doc.LoadXml(artifactResolveBody);
    
                SignedXml signedXml  = new SignedXml(doc);
                signedXml.SigningKey = certificate.PrivateKey;
                signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl; //"http://www.w3.org/2001/10/xml-exc-c14n#";
    
                // Create a reference to be signed.
                Reference reference = new Reference();
                reference.Uri       = id;
    
                // Add an enveloped transformation to the reference.            
                XmlDsigEnvelopedSignatureTransform env =
                   new XmlDsigEnvelopedSignatureTransform(true);
                reference.AddTransform(env);
    
                XmlDsigExcC14NTransform c14n = new XmlDsigExcC14NTransform();
                c14n.Algorithm = SignedXml.XmlDsigExcC14NTransformUrl;
                reference.AddTransform(c14n);
    
                // Add the reference to the SignedXml object.
                signedXml.AddReference(reference);
    
                // Compute the signature.
                signedXml.ComputeSignature();
    
                // Get the XML representation of the signature and save 
                // it to an XmlElement object.
                XmlElement xmlDigitalSignature = signedXml.GetXml();
    
                // the signature does directly between Issuer and Artifact
                doc.DocumentElement.AppendChild(doc.ImportNode(xmlDigitalSignature, true));
    
                //var artifactChild = doc.DocumentElement.ChildNodes[doc.DocumentElement.ChildNodes.Count-1];
                //doc.DocumentElement.InsertBefore(doc.ImportNode(xmlDigitalSignature, true), artifactChild);
    
                // soap enveloped
                return CreateSoapEnvelope( doc.OuterXml );
            }
    
            private string CreateSoapEnvelope( string soapBodyContent )
            {
                if (string.IsNullOrEmpty(soapBodyContent)) throw new ArgumentNullException();
                return string.Format("<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\"><s:Body>{0}</s:Body></s:Envelope>", 
                       soapBodyContent );
            }
    
  10. (this was slightly tricky) when sending the SOAP Envelope with the ArtifactResolve to ADFS, make sure you add the SOAPAction header set to http://www.oasis-open.org/committees/security. Also make sure you set the Content-Type to text/xml

A correct ArtifactResolve request makes ADFS return the ArtifactResponse document which looks more or less like

<s:Envelope xmlns:s=\"http://schemas.xmlsoap.org/soap/envelope/\">
 <s:Body>
  <samlp:ArtifactResponse ID=\"_d9f59a16-8189-4095-9f57-1033439cb1f3\" Version=\"2.0\" IssueInstant=\"2017-09-11T10:20:49.527Z\" 
                        Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"_id\" xmlns:samlp=\"urn:oasis:names:tc:SAML:2.0:protocol\">
   <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://adfs.local/adfs/services/trust</Issuer>
   <samlp:Status>
    <samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" />
   </samlp:Status>
   <samlp:Response ID=\"_e0d8b313-ba58-4325-a9af-39a406a73bc8\" Version=\"2.0\" IssueInstant=\"2017-09-11T10:20:49.277Z\" Destination=\"https://rp.com\" 
       Consent=\"urn:oasis:names:tc:SAML:2.0:consent:unspecified\" InResponseTo=\"guid_2e0575b5-37b5-4546-a209-1bbc725c6532\">
    <Issuer xmlns=\"urn:oasis:names:tc:SAML:2.0:assertion\">http://adfs.local/adfs/services/trust</Issuer>
    <samlp:Status>
     <samlp:StatusCode Value=\"urn:oasis:names:tc:SAML:2.0:status:Success\" />
    </samlp:Status>
    <Assertion ID=\"_04f54457-70ce-4850-a639-c068a0f801db\" IssueInstant=\"2017-09-11T10:20:49.277Z\" ...>
     <Issuer>http://adfs.local/adfs/services/trust</Issuer>
     <ds:Signature xmlns:ds=\"http://www.w3.org/2000/09/xmldsig#\">...</ds:Signature>
     <Subject>...</Subject>
     <Conditions">...</Conditions>
     <AttributeStatement>
      <Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name\">
       <AttributeValue>foo
      </Attribute>
      <Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname\">
       <AttributeValue>Foo
      </Attribute>
      <Attribute Name=\"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname\">
       <AttributeValue>Bar
      </Attribute>
     </AttributeStatement>
     <AuthnStatement AuthnInstant=\"2017-09-11T10:20:49.199Z\">
      <AuthnContext>
       <AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</AuthnContextClassRef>
      </AuthnContext>
     </AuthnStatement>
    </Assertion>
   </samlp:Response>
  </samlp:ArtifactResponse>
 </s:Body>
</s:Envelope>
The response has to be extracted from the SOAP Envelope and verified (assertions are signed). Then, it can be passed down the pipeline as if it comes from any other binding.

No comments: