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:
Name | Description | Value 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/ |
AppKey | The Application key identifying your application to Saxo Bank - this was provided to you when you registered | ABCDE12345 |
AppSecret | The 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.
What | Description | Value 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.
What | Description |
---|---|
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")
- A SAML2 EncryptedAssertion, encrypted with the Saxo Bank Encryptiopn certificate public key. The assertion carries the "secrets" used to authentication and token issuance:
- 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 AuthnRequest carrying info about the issuer & destination of the authentication, and a token:
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):
Looking at the the assertion, before it is encrypted, it contains the following:
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):
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:
- Correctly create the assertion,
- Encrypt it and put it in the token response and
- Sign the entire response
The assertion is created by building the xml like this:
// 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:
// 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:
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:
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 :-):
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:
// 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:
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);