OpenAPI

Introduction

This section explains the login & authentication flow of web-based application.

Overall, the login process consists of 2 steps:

  1. Authentication - Single SignOn

  2. Acquiring an Access Token

Step 2 is identical to the process used by a native application, but the first step is a bit different.

But before you can get started, the application must be registered, which would provide the following pieces of 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.

http://www.mytestsite.com/
AuthenticationUrlThe URL of the Saxo Bank authentication & authorization server - this was provided to you when you registered
https://www.logonvalidation.net/
AppKeyThe Application key identifying your application to Saxo Bank - this was provided to you when you registered-
AppSecretThe Application "secret" identifying your application to Saxo Bank - this was provided to you when you registered
OpenApiBaseUrlBase URL for calling OpenAPI REST endpoints.

Never expose your AppKey and AppSecret to the users of your app - read our recommendations on security regarding AppKey and AppSecret here.

Authentication - Single SignOn

The first step in the authentication process, is sending a SAML Authentication request to SSO. This is a POST request, containing a Base64 encoded SAML request as shown below:

SAML Authentication Request
<samlp:AuthnRequest ID="_a04fb772-b9bf-4779-96d8-48843c9d3695" Version="2.0" ForceAuthn="false"  IsPassive="false" 
 ProtocolBinding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST" xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol"
 IssueInstant="2015-07-02T13:41:59Z" Destination="http://www.logonvalidation.net/AuthnRequest"  
 AssertionConsumerServiceURL="http://www.mytestsite.com/default.aspx" >
 <samlp:NameIDPolicy AllowCreate="false" />
 <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">http://www.mytestsite.com/</saml:Issuer>
</samlp:AuthnRequest>

In above code block, these fields are of important relevance:

FieldDescriptionline no. in example
IDA unique ID - in the example above it is a guid preceded by a "_"1
IssueInstantThe Request time (UTC, ISO 8601 formatted)3
Destination

The URL of the authentication endpoint (the same that the request is actually sent to).

Destination = AuthenticationUrl  + "/AuthnRequest".

3
AssertionConsumerServiceURL

This is Application URL aka the URL of the page that should receive the authentication response. This must be a page under the service provider URL that your application has been registered with in Saxo Bank.

4
saml:Issuer (tag contents)The URL of the page issuing the request.6

The SAML request, must be POSTed as SAMLRequest to the authentication endpoint which is AuthenticationUrl + "/AuthnRequest" (e.g. https://sim.logonvalidation.net/AuthnRequest for the SIM environment). For more info on various environments, visit - Link.

Below is the code construct from example to generate the request SAML: 

SAML Generation
var timestamp = $"{DateTime.UtcNow.ToString("s", CultureInfo.InvariantCulture)}Z";
            
return $@"
         <samlp:AuthnRequest ID=""_{Guid.NewGuid()}"" Version=""2.0"" ForceAuthn=""false"" IsPassive=""false""
         ProtocolBinding=""urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"" xmlns:samlp=""urn:oasis:names:tc:SAML:2.0:protocol""
         IssueInstant=""{timestamp}"" Destination=""{authenticationUrl}"" AssertionConsumerServiceURL=""{applicationUrl}"">
         <samlp:NameIDPolicy AllowCreate=""false"" />
         <saml:Issuer xmlns:saml=""urn:oasis:names:tc:SAML:2.0:assertion"">{issuerUrl}</saml:Issuer>
         </samlp:AuthnRequest>";


The example code uses a hidden form for POSTing the request - but this is of course is much easier in a JavaScript based single page app.

If the user is not already logged in to SSO, it will redirect the user agent to the login dialog (which is why the request should be sent from the browser itself, and not server-to-server).
After the authentication is finished (whether it required the login dialog or not), SSO posts the result back to the page specified in the "AssertionConsumerServiceURL".
The POST request contains a single field: "SAMLResponse", which contains a Base64 encoded SAML response like the one showed here (certificate and signature tags truncated for the sake of brevity):

Authentication SAML Response
<samlp:Response ID="_6d05f0db-136f-4572-86df-30ce091a99d5" InResponseTo="_6bfc03cb-f6aa-47db-8720-155faa23a2e0" Version="2.0" IssueInstant="2015-06-29T15:10:55Z" Destination="http://www.mytestsite.com" 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="#_6d05f0db-136f-4572-86df-30ce091a99d5">
				<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>fkj32atFDEmSDF5637wnsY=</DigestValue>
			</Reference>
		</SignedInfo>
		<SignatureValue>m4oVXG [Truncated] hXrOQ==</SignatureValue>
		<KeyInfo>
			<X509Data>
				<X509Certificate>MlOF9TC [Truncated] FmU=</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="_f64ecdca-9814-411f-9611-ab2cef12373e" IssueInstant="2015-06-29T15:10:55Z" 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">AvkPuVdwkLAdpf3UBT5JPg==</saml:NameID>
			<saml:SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:cm:bearer">
				<saml:SubjectConfirmationData Recipient="http://www.mytestsite.com" InResponseTo="_6bfc03cb-f6aa-47db-8720-155faa23a2e0" /></saml:SubjectConfirmation>
		</saml:Subject>
		<saml:Conditions NotOnOrAfter="2015-06-29T15:11:55Z">
			<saml:AudienceRestriction>
				<saml:Audience>http://www.mytestsite.com</saml:Audience>
			</saml:AudienceRestriction>
		</saml:Conditions>
		<saml:AuthnStatement AuthnInstant="2015-06-29T15:10:55Z" SessionIndex="dc3fbeac8eb2453b971010e180d187fe1655287" SessionNotOnOrAfter="2015-06-29T15:12:25Z">
			<saml:AuthnContext>
				<saml:AuthnContextClassRef>urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport</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>False</saml:AttributeValue>
			</saml:Attribute>
		</saml:AttributeStatement>
		<saml:AttributeStatement>
			<saml:Attribute Name="AuthorizationCode" NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:basic">
				<saml:AttributeValue>3ab31ab4-98da-33d0-40fa-3af4abb11184</saml:AttributeValue>
			</saml:Attribute>
		</saml:AttributeStatement>
	</saml:Assertion>
</samlp:Response>

This response contains info for verifying the validity of the response (and sender) and that of the intended "audience" (this should be your site) – but for the sake of requesting the token, it is the AuthorizationCode attribute value that is important because this is what we use in next step: Acquiring an Access Token. 

The AuthorizationCode value node can be found using the XPath expression: "/samlp:Response/saml:Assertion/saml:AttributeStatement/saml:Attribute[@Name='AuthorizationCode']/saml:AttributeValue".

Get Access Token

Having the authorization code in hand, we can now exchange this for an Access Token by sending POST request to authorization url which is AuthenticationUrl  + "/token" (e.g. for simulation its https://sim.logonvalidation.net/token i.e. AuthenticationUrl + "/token" ).

POST request must contain the following:

  • The endpoint to this is "https://sim.logonvalidation.net/token" (for simulation)
  • The POST request takes the parameters grant_type=authorization_code & code=<the authorization code from SSO>
  • The authorization header contains the application key and app secret that you receive from Saxo Bank upon registering you application. The "<appKey>:<appSecret>" pair is Base64 encoded and prefixed by "Basic "


string requestPayload = "grant_type=authorization_code&code=" + authorizationCode;

HttpWebRequest request = WebRequest.CreateHttp("https://sim.logonvalidation.net/token");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(String.Format("{0}:{1}", appKey, secret))));

byte[] buffer = Encoding.UTF8.GetBytes(requestPayload);
request.ContentLength = buffer.Length;

using (Stream requestStream = request.GetRequestStream())
{
    requestStream.Write(buffer, 0, buffer.Length);
}

HttpWebResponse response = (HttpWebResponse) request.GetResponse()


The response from the token endpoint is a JSON object containing Access Token as well as a Refresh Token which is used for acquiring more tokens when the current one expires:

{
  "access_token": "eyJ [Truncated ] RA",
  "token_type": "Bearer",
  "expires_in": 1200,
  "refresh_token": "05aa3bcd-ab33-1142-91d3-50affbba43a3",
  "refresh_token_expires_in": 3600,
  "base_uri": null
}

The access_token (and token_type) values are used for calling OpenApi, and should be placed in some kind of persistent storage, to avoid unnecessary re-authentication. 

The refresh_token value is used for getting a replacement token when the existing one is expiring. "expires_in" and "refresh_token_expires_in" are integer values that contain the expiry intervals (in seconds) for token and refresh token respectably.

Using Access Token

When the client application needs to invoke an OpenApi service, the token & token type are combined and put into the Authorization headers like this:

HttpWebRequest oaRequest = WebRequest.CreateHttp("https://gateway.saxobank.com/openapi/port/v1/clients/me");
oaRequest.Headers.Add(HttpRequestHeader.Authorization, string.Format("{0} {1}", token.TokenType, token.AccessToken));
HttpWebResponse response = (HttpWebResponse) oaRequest.GetResponse();

Refresh Access Token

Since the Access Token has a limited lifespan, it needs to be refreshed at regular intervals. For the same, token response contains a refresh token that has a longer expiry than the token itself and that can be exchanged for a new token.

The request for a new token is made against the same endpoint that the original token request was made, the only difference to the original request is the payload of the request as can be seen in below snapshot:

string requestPayload = "grant_type=refresh_token&refresh_token=" + refreshToken;

HttpWebRequest request = WebRequest.CreateHttp("https://sim.logonvalidation.net/token");
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";
request.Headers.Add(HttpRequestHeader.Authorization, "Basic " + Convert.ToBase64String(Encoding.UTF8.GetBytes(String.Format("{0}:{1}", appKey, secret))));

byte[] buffer = Encoding.UTF8.GetBytes(requestPayload);
request.ContentLength = buffer.Length;

using (Stream requestStream = request.GetRequestStream())
{
    requestStream.Write(buffer, 0, buffer.Length);
}

HttpWebResponse response = (HttpWebResponse) request.GetResponse()

In the payload of the refresh request, the grant_type is set to refresh_token and the actual refresh token is provided in a parameter named refresh_token, but the rest of the request is identical - it also needs the appKey and -secret to identify the application, and the returned JSON structure is identical - so you get a new token and a new refresh token.

Authentication Sequence Diagram

Sequence diagram of login flow is shown below:

Downloads

Download Sample Application