OpenAPI

Introduction

This article explains the process of getting an OpenAPI Access Token using certificate-based authentication. Certificate based authentication is intended for long-running services which need to access OpenAPI without a user present to perform the login.

Applicable scenarios include:

  • A service calling our Onboarding API to upload lead information.
  • A service calling our Portfolio API to obtain regular updates on client balances and margin utilization.
     

Currently Certificate-based Authentication is only available in our LIVE environment, and it is only available to select IBs and White Label Clients upon request.

The certificate based authentication-process only consists of a single step.

The calling application transmits a signed and encrypted request to Saxo Bank authentication & authorization server containing: 

  • a Userid, which identifies a user in the Saxo Bank system.
  • and set of AppKey & AppSecret which identifies the calling application.

In return, application receives an OpenAPI Access Token,  and optionally a refresh token, so it can keep the OpenAPI session alive without further need for re-transmission of certificate related information (except for re-connection purposes).

Prerequisites

Before you proceed further with developing your app, you need following:

Application Credentials

Your application must be registered with Saxo Bank, and you must have received from us, the following information:

NameDescriptionValue used in the examples below

AppUrl

A URL uniquely representing your app - this was provided to you when you registered for the developer license.

https://www.logonvalidation.net/MyTestApp

AuthenticationUrl

The URL of the Saxo Bank authentication & authorization server - this was provided to you when you registered
https://www.logonvalidation.net/

PartnerIdpUrl

A URL Defining your partner configuration with Saxo Bank, provided to you by Saxo Bank

http://example.partnerIdpURL.com/
AppKeyThe Application key identifying your application to Saxo Bank - this was provided to you when you registeredABCDE12345
AppSecretThe Application "secret" identifying your application to Saxo Bank - this was provided to you when you registered

12345ABCDE

User Credentials

Ensure you already have got your user created in Saxo Bank system with correct access rights and that you also have a client certificate for that user.

WhatDescriptionValue used in the examples below
UserId

This is the user, which will be used by your application to log into the Saxo Bank System.

12345

clientCertNumber

(Client Certificate)

The certificate used to sign the SAML request. The certificate is created by Saxo Bank, and bound to a single user-id
- 

Please contact openapi@saxobank.com to request userid and client certificate.

Saxo Bank Encryption Certificate

This certificate is used to encrypt SAML assertion within the SAML response, before it is signed by the Client Certificate.

WhatDescription

saxoCertNumber

(Saxo Bank encryption certificate)

The public certificate used for encrypting the request, to ensure that it's only accessible to Saxo Bank.

You may obtain this certificate at the following location:

https://live.logonvalidation.net/metadata (Look for the path: 'md:KeyDescriptor use="encryption"')

This text can be copied to a new text file and saved with .cer extension. Windows should now be able to open the certificate.

Note that both certificates have a limited lifetime (typically 1-2 years). It is your responsiblity to ensure that you request new certificates before exisiting one's expire.

SAML2 Library

We strongly recommend that you use a professional SAML/SSO library for composing the SAML response and ensuring that it is formatted and signed correctly.

Our sample app have been written without using a library (using only native XML operations) to show the specifics of the implementation, but by using a 3rd party library, your implementation will  be easier to manage, and less vulnerable to implementation errors that could introduce security weaknesses.

Implementing Certificate Based Authentication

The flow is actually quite simple:

The client application creates a SAML Response and POST it to Saxo Bank's authentication URL which is AuthenticationUrl + "/AuthnRequest" (e.g. https://live.logonvalidation.net/AuthnRequest for the LIVE environment). For more info on various environments, visit - Link.

The SAML response contains:

  • A valid user id and
  • Service applicationkey and application secret, which is signed by client certificate and encrypted by Saxo Bank encryption certificate.

In return the application will receive an access token and an optional refreshtoken. The application may use the access token to call OpenAPI endpoints.

Once the access token expires:

  • If the application has also received a refresh token, then it can use that to get a new access token.
  • if the application has not received a refresh token, it must go through the initial process of submitting a SAML response.

Structure of the Request

The authorization request consists of the following layers (from the outside and in):

  • A simple SOAP wrapper, for transport purposes, must be sent as a POST request with the content-type: "application/soap+xml".
    • A SAML2 AuthnRequest carrying info about the issuer & destination of the authentication, and a token:
      • The token consists of a SAML2 Response, carrying information abut the token issuer (the partner itself), and signed with the Client Certificate private key.
        • A SAML2 EncryptedAssertion, encrypted with the Saxo Bank Encryptiopn certificate public key. The assertion carries the "secrets" used to authentication and token issuance:
          • User Id (Sent as the NameId of the assertion)
          • App Key (as an attribute)
          • App Secret (as an attribute)
          • A flag telling that this assertion represents Certificate Based Authentication (the attribute "IsCertificateBasedAuthentication", with the value "True")


The full request sent to the authentication server looks something like this (digests, ciphers and other large data-blocks have been truncated for the sake of brevity): 

Authentication Request XML
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" soap:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
    <soap:Body>
        <samlp:AuthnRequest ID="_96b77ead-287c-4dee-a37f-d318784401a2" Version="2.0" IssueInstant="2016-02-29T09:10:05Z" Destination="https://www.logonvalidation.net/MyTestApp" ForceAuthn="false" IsPassive="false" ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" AssertionConsumerServiceURL="https://www.logonvalidation.net/MyTestApp" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
            <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://idp.partner.com/</saml:Issuer>
            <samlp:Extensions>
                <Token>
                    <samlp:Response ID="_a2e918be-0d58-4412-9a47-2d587545b6ba" Version="2.0" IssueInstant="2016-02-29T09:10:05Z" Destination="https://www.logonvalidation.net/MyTestApp" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
                        <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://idp.partner.com/</saml:Issuer>
                        <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
                            <SignedInfo>
                                <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                                <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
                                <Reference URI="#_a2e918be-0d58-4412-9a47-2d587545b6ba">
                                    <Transforms>
                                        <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                                        <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                                            <InclusiveNamespaces PrefixList="#default samlp saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" />
                                        </Transform>
                                    </Transforms>
                                    <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                                    <DigestValue>76d...k=</DigestValue>
                                </Reference>
                            </SignedInfo>
                            <SignatureValue>axt3...Jg==</SignatureValue>
                            <KeyInfo>
                                <X509Data>
                                    <X509Certificate>P9La...A77</X509Certificate>
                                </X509Data>
                            </KeyInfo>
                        </Signature>
                        <samlp:Status>
                            <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
                        </samlp:Status>
                        <saml:EncryptedAssertion xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
                            <EncryptedData Type="http://www.w3.org/2001/04/xmlenc#Element" xmlns="http://www.w3.org/2001/04/xmlenc#">
                                <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes256-cbc" />
                                <KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">
                                    <EncryptedKey xmlns="http://www.w3.org/2001/04/xmlenc#">
                                        <EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-1_5" />
                                        <CipherData>
                                            <CipherValue>t66A...Xcg==</CipherValue>
                                        </CipherData>
                                    </EncryptedKey>
                                </KeyInfo>
                                <CipherData>
                                    <CipherValue>zS223...5TTg=</CipherValue>
                                </CipherData>
                            </EncryptedData>
                        </saml:EncryptedAssertion>
                    </samlp:Response>
                </Token>
            </samlp:Extensions>
            <samlp:NameIDPolicy AllowCreate="false" />
        </samlp:AuthnRequest>
    </soap:Body>
</soap:Envelope>

Looking at the the assertion, before it is encrypted, it contains the following:

SAML2 Assertion XML
<saml:Assertion Version="2.0" ID="_6983c531-d305-4977-ad20-ef292885ec11" IssueInstant="2016-02-29T09:22:45Z" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
    <saml:Issuer>http://idp.partner.com/</saml:Issuer>
    <saml:Subject>
        <saml:NameID>12345</saml:NameID>
        <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
            <saml:SubjectConfirmationData Recipient="https://www.logonvalidation.net/MyTestApp" />
        </saml:SubjectConfirmation>
    </saml:Subject>
    <saml:Conditions NotOnOrAfter="2016-02-29T09:24:45Z">
        <saml:AudienceRestriction>
            <saml:Audience>https://www.logonvalidation.net/MyTestApp</saml:Audience>
        </saml:AudienceRestriction>
    </saml:Conditions>
    <saml:AuthnStatement AuthnInstant="2016-02-29T09:22:45Z">
        <saml:AuthnContext>
            <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
        </saml:AuthnContext>
    </saml:AuthnStatement>
    <saml:AttributeStatement>
        <saml:Attribute Name="IsCertificateBasedAuthentication" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
            <saml:AttributeValue>True</saml:AttributeValue>
        </saml:Attribute>
    </saml:AttributeStatement>
    <saml:AttributeStatement>
        <saml:Attribute Name="AppKey" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
            <saml:AttributeValue>AAAAAAAA</saml:AttributeValue>
        </saml:Attribute>
    </saml:AttributeStatement>
    <saml:AttributeStatement>
        <saml:Attribute Name="AppSecret" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
            <saml:AttributeValue>BBBBBBBB</saml:AttributeValue>
        </saml:Attribute>
    </saml:AttributeStatement>
</saml:Assertion>

The mechanism of signing the token, and encrypting the assertion, follows that of the SAML2 standard.

Response Message

The Authentication Server responds with a SOAP message of its own, containing a SAML2 Response, with either an error code or an OpenAPI Access Token (which is a JSON object):

Authentication Response XML
<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/">
    <SOAP-ENV:Body>
        <samlp:Response ID="_33547e25-2de9-4b28-8ddf-16767095679a" InResponseTo="_96b77ead-287c-4dee-a37f-d318784401a2" Version="2.0" IssueInstant="2016-02-29T09:10:08Z" Destination="https://www.logonvalidation.net/MyTestApp" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
            <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://www.logonvalidation.net/</saml:Issuer>
            <Signature xmlns="http://www.w3.org/2000/09/xmldsig#">
                <SignedInfo>
                    <CanonicalizationMethod Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#" />
                    <SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1" />
                    <Reference URI="#_33547e25-2de9-4b28-8ddf-16767095679a">
                        <Transforms>
                            <Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature" />
                            <Transform Algorithm="http://www.w3.org/2001/10/xml-exc-c14n#">
                                <InclusiveNamespaces PrefixList="#default samlp saml ds xs xsi" xmlns="http://www.w3.org/2001/10/xml-exc-c14n#" />
                            </Transform>
                        </Transforms>
                        <DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1" />
                        <DigestValue>Pd4...6Ts=</DigestValue>
                    </Reference>
                </SignedInfo>
                <SignatureValue>D3a...ZcW7==</SignatureValue>
                <KeyInfo>
                    <X509Data>
                        <X509Certificate>TVss1..Bo=</X509Certificate>
                    </X509Data>
                </KeyInfo>
            </Signature>
            <samlp:Status>
                <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success" />
            </samlp:Status>
            <saml:Assertion Version="2.0" ID="_d6f19626-e34f-4c07-9b72-684525e6324d" IssueInstant="2016-02-29T09:10:08Z" xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">
                <saml:Issuer>https://www.logonvalidation.net/</saml:Issuer>
                <saml:Subject>
                    <saml:NameID Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified">z/Pu73UTZDhdH4JqyVi2DA==</saml:NameID>
                    <saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
                        <saml:SubjectConfirmationData Recipient="https://www.logonvalidation.net/MyTestApp" InResponseTo="_96b77ead-287c-4dee-a37f-d318784401a2" />
                    </saml:SubjectConfirmation>
                </saml:Subject>
                <saml:Conditions NotOnOrAfter="2016-02-29T09:11:08Z">
                    <saml:AudienceRestriction>
                        <saml:Audience>https://www.logonvalidation.net/MyTestApp</saml:Audience>
                    </saml:AudienceRestriction>
                </saml:Conditions>
                <saml:AuthnStatement AuthnInstant="2016-02-29T09:10:08Z" SessionIndex="052c5ac8ae8b4d54bf0829c6bc4011055666954" SessionNotOnOrAfter="2016-02-29T09:20:08Z">
                    <saml:AuthnContext>
                        <saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:Password</saml:AuthnContextClassRef>
                    </saml:AuthnContext>
                </saml:AuthnStatement>
                <saml:AttributeStatement>
                    <saml:Attribute Name="IsFederated" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                        <saml:AttributeValue>True</saml:AttributeValue>
                    </saml:Attribute>
                </saml:AttributeStatement>
                <saml:AttributeStatement>
                    <saml:Attribute Name="IPAddress" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                        <saml:AttributeValue>fe...%3</saml:AttributeValue>
                    </saml:Attribute>
                </saml:AttributeStatement>
                <saml:AttributeStatement>
                    <saml:Attribute Name="OpenApiToken" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
                        <saml:AttributeValue>{"access_token":"eyJ8... Yhj","token_type":"Bearer","expires_in":1000,"refresh_token":"94...aa","refresh_token_expires_in":3600,"base_uri":null}</saml:AttributeValue>
                    </saml:Attribute>
                </saml:AttributeStatement>
            </saml:Assertion>
        </samlp:Response>
    </SOAP-ENV:Body>
</SOAP-ENV:Envelope>
TIP: The token element can be found using the XPath selector: "//saml:Attribute[@Name='OpenApiToken']/saml:AttributeValue".

Implementing CBA

To illustrate handling of the SAML protocol (especially when it comes to encryption and signing) we have chosen not to use a 3rd party framework for generating the XML. The core task of implementing Certificate Based Authentication in your app is to:

  1. Correctly create the assertion,
  2. Encrypt it and put it in the token response and
  3. Sign the entire response

The assertion is created by building the xml like this:

Creating the Assertion
// Setup all the namespaces
private static readonly XNamespace SoapNs = "http://schemas.xmlsoap.org/soap/envelope/";
private static readonly XNamespace SamlNs = "urn:oasis:names:tc:SAML:2.0:assertion";
private static readonly XNamespace SamlpNs = "urn:oasis:names:tc:SAML:2.0:protocol";
private static readonly XNamespace XmlDsigNs = "http://www.w3.org/2000/09/xmldsig#";
private static readonly XNamespace XmlEncNs = "http://www.w3.org/2001/04/xmlenc#";
//Create the SAML assertion containing the secrets
string assertionId = "_" + Guid.NewGuid();
string assertionIssuingTime = string.Format("{0:s}Z", DateTime.UtcNow);
string assertionExpiryTime = string.Format("{0:s}Z", DateTime.UtcNow.AddMinutes(2));
XElement assertion =
    new XElement(SamlNs + "Assertion",
        new XAttribute(XNamespace.Xmlns + "saml", SamlNs),
        new XAttribute("Version", "2.0"),
        new XAttribute("ID", assertionId),
        new XAttribute("IssueInstant", assertionIssuingTime),
        new XElement(SamlNs + "Issuer",
            partnerIdpUrl),
        new XElement(SamlNs + "Subject",
            new XElement(SamlNs + "NameID",
                userId
                ),                        
            new XElement(SamlNs + "SubjectConfirmation",
                new XAttribute("Method", "urn:oasis:names:tc:SAML:2.0:cm:bearer"),
                new XElement(SamlNs + "SubjectConfirmationData",
                    new XAttribute("Recipient", destination)
                    )
                )
            ),
        new XElement(SamlNs + "Conditions",
            new XAttribute("NotOnOrAfter", assertionExpiryTime),
            new XElement(SamlNs + "AudienceRestriction",
                new XElement(SamlNs + "Audience",
                    destination
                    )
                )
            ),
        new XElement(SamlNs + "AuthnStatement",
            new XAttribute("AuthnInstant", assertionIssuingTime),
            new XElement(SamlNs + "AuthnContext",
                new XElement(SamlNs + "AuthnContextClassRef",
                    "urn:oasis:names:tc:SAML:2.0:ac:classes:Password"
                    )
                )
            ),
        new XElement(SamlNs + "AttributeStatement",
            new XElement(SamlNs + "Attribute",
                new XAttribute("Name", "IsCertificateBasedAuthentication"),
                new XAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"),
                new XElement(SamlNs + "AttributeValue",
                    true
                    )
                )
            ),
        new XElement(SamlNs + "AttributeStatement",
            new XElement(SamlNs + "Attribute",
                new XAttribute("Name", "AppKey"),
                new XAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"),
                new XElement(SamlNs + "AttributeValue",
                    appKey
                    )
                )
            ),
        new XElement(SamlNs + "AttributeStatement",
            new XElement(SamlNs + "Attribute",
                new XAttribute("Name", "AppSecret"),
                new XAttribute("NameFormat", "urn:oasis:names:tc:SAML:2.0:attrname-format:basic"),
                new XElement(SamlNs + "AttributeValue",
                    appSecret
                    )
                )
            )
        );

The assertion is then encrypted and the encrypted element converted back to XML:

Encrypting the Assertion
// Encrypt the assertion
XmlDocument doc = new XmlDocument();
XmlElement assertionXmlEl = doc.ReadNode(assertion.CreateReader()) as XmlElement;
EncryptedXml eXml = new EncryptedXml();
if (assertionXmlEl == null)
    throw new NullReferenceException("assertionXmlEl was null");
// Encrypt the element.
EncryptedData encryptedElement = eXml.Encrypt(assertionXmlEl, encryptionCert);

XElement encryptedAssertionXElement = XElement.Parse(encryptedElement.GetXml().OuterXml);

Now it's time to construct the SAML-Response XML:

Create the Response
XElement samlResponse = 
    new XElement(SamlpNs + "Response",
        new XAttribute(XNamespace.Xmlns + "saml", SamlNs),
        new XAttribute(XNamespace.Xmlns + "samlp", SamlpNs),
        new XAttribute("ID", responseId),
        new XAttribute("Version", "2.0"),
        new XAttribute("IssueInstant", issueTime),
        new XAttribute("Destination", destination),
        new XElement(SamlpNs + "Issuer",
            partnerIdpUrl
            ),                    
        new XElement(SamlpNs + "Status",
            new XElement(SamlpNs + "StatusCode",
                new XAttribute("Value", "urn:oasis:names:tc:SAML:2.0:status:Success")
                )
            ),
        new XElement(SamlNs + "EncryptedAssertion",
            encryptedAssertionXElement 
        )
    );

and now the response must be signed, and the signature inserted into the response:

Signing the response
string responseId = samlResponse.Attribute("ID").Value;

XmlDocument doc = new XmlDocument();
XmlElement samlResponseXmlEl = doc.ReadNode(samlResponse.CreateReader()) as XmlElement;
doc.AppendChild(samlResponseXmlEl);

if (samlResponseXmlEl == null)
    throw new NullReferenceException("samlResponseXmlEl was null");

var signedXml = new SignedXml(doc);

var signatureReference = new Reference("#" + responseId);
signatureReference.AddTransform(new XmlDsigExcC14NTransform("#default samlp saml ds xs xsi"));
signatureReference.AddTransform(new XmlDsigEnvelopedSignatureTransform());

signedXml.AddReference(signatureReference);

signedXml.SigningKey = clientCert.PrivateKey;

var certificateKeyInfo = new KeyInfo();
certificateKeyInfo.AddClause(new KeyInfoX509Data(clientCert));
signedXml.KeyInfo = certificateKeyInfo;
signedXml.SignedInfo.CanonicalizationMethod = SignedXml.XmlDsigExcC14NTransformUrl;
signedXml.ComputeSignature();

string signatureXml = signedXml.GetXml().OuterXml;
 
//Adding into saml response
samlResponse.AddFirst(XElement.Parse(signatureXml));

Inserting the token into a SAML Request and wrapping it in SOAP can be done in XML but could also be done using simple String.Format - we do a bit of both :-):

Create SAML Request & SOAP wrappers
XElement authnRequest = 
        new XElement(SamlpNs + "AuthnRequest",
            new XAttribute(XNamespace.Xmlns + "samlp", SamlpNs),
            new XAttribute(XNamespace.Xmlns + "saml", SamlNs),
            new XAttribute("ID", authnRequestId),
            new XAttribute("IssueInstant", issueTime),
            new XAttribute("Destination", destination),
            new XAttribute("AssertionConsumerServiceURL", destination),
            new XAttribute("Version", "2.0"),
            new XAttribute("ForceAuthn", false),
            new XAttribute("IsPassive", false),
            new XAttribute("ProtocolBinding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
            new XElement(SamlNs + "Issuer",
                partnerIdpUrl
            ),
            new XElement(SamlpNs + "Extensions",
                new XElement("Token",
                    samlResponse
                )
            ),
            new XElement(SamlpNs + "NameIDPolicy",
                new XAttribute("AllowCreate", false)
            )
        );
    
string authnRequestString = authnRequest.ToString(SaveOptions.DisableFormatting);

 
//Wrapping it into soaprequest
string soapRequest = String.Format("<?xml version=\"1.0\" encoding=\"utf-8\"?>" +
                        "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" " +
                        "soap:encodingStyle=\"http://schemas.xmlsoap.org/soap/encoding/\">" +
                        "<soap:Body>{0}</soap:Body></soap:Envelope>", authnRequestString);

After this only thing to remember is to set the ContentType to "application/soap+xml" before transmitting the request:

Transmit Request
// Initialize httpClient with cookie container to ensure stickiness and automatic decompression of recieved data. Note that in production code this must be disposed correctly
             
HttpClient httpClient = new HttpClient(
               new HttpClientHandler
                 {
                    CookieContainer = new CookieContainer(),
                    AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate,
                    UseDefaultCredentials = true
                 });
            
// We need to set the content-type without getting a chartset value as well
HttpContent content =  new StringContent(samlRequest);
content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/soap+xml");
            
HttpResponseMessage response = await httpClient.PostAsync(new Uri(destinationUrl), content);

After that, it is only a matter of parsing the response, and using the OpenApi token as described in this section:

Parse Token Response
XmlNamespaceManager xmlns = new XmlNamespaceManager(soapResponseXml.OwnerDocument.NameTable);
xmlns.AddNamespace("saml", "urn:oasis:names:tc:SAML:2.0:assertion");
XmlNode oaTokenNode = soapResponseXml.SelectSingleNode("//saml:Attribute[@Name='OpenApiToken']/saml:AttributeValue", xmlns);

Downloads

Download Sample Application