OpenAPI

Introduction

This section explains login & authentication flow of a native-app-based application. Overall, the login process consists of 2 steps:

  1. Authentication - Single SignOn.

  2. Get Access Token.

Step 2 is identical to the process used by a web-based 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.
For native-apps the URL is typically a URL referring to Saxo Bank.

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/
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

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

Authorizing a native app against SSO requires app to embed a browser control, because login flow if necessary redirects request to relevant login-page which returns a response containing SAML token embedded in its HTML(which must then be read from the browser, by the app).

The first step in the authorization 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" IssueInstant="2015-07-02T13:41:59Z" 
 Destination="http://www.logonvalidation.net/AuthnRequest"  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">https://www.logonvalidation.net/MyTestApp</saml:Issuer>
 <samlp:NameIDPolicy AllowCreate="false" />
</samlp:AuthnRequest>

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

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

The URL of the authentication endpoint (where request is actually sent to)

Destination = AuthenticationUrl + "/AuthnRequest".

2
AssertionConsumerServiceURL

This is used for telling SSO that the application trying to log in is your native app, it must be set to your Application URL, as it identifies your app uniquely

4
saml:Issuer (tag contents)The URL of the page issuing the request - this is set to the Application URL to signal that your app is issuing the request5

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>"; 

In the example app, this is sent using the System.WebBrowser.Navigate() method.

The POST request must be sent with content-type: application/x-www-form-urlencoded, and the SAML-request itself must be Base64 encoded and then URLEncoded (this last step is normally done by a normal form-submission in a browser, but if you post the request using a method like WebBrowser.Navigate() this must be done manually).

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 renders a page containing the SAML response embedded in the BODY tag.

The process of detecting the "end state" and retrieving the actual SAML Response requires looking at each page the browser control receives, and check for a META tag on the page to determine the application state. The tag looks like this:

<meta name="Application-State" content="service=IDP;federated=False;env=Test;state=Ok;authenticated=False;">

The state we are specifically interested in is the one where:

  • service=IDP
  • state=Token
  • authenticated=True

If the service is not IDP, the login process has not reached the IDP yet, and the page can be safely ignored. When the authentication completes, the authenticated value becomes True and the state becomes "Token" which means that page contains embedded token. If it has state=Ok and authenticated=True it is typically a sign that your app was registered as a website instead of a native app, which can be resolved by contacting Saxo Bank to change your registration.

The "state=Token" page looks something like this (the token is a lot larger than the snippet shown here):

<HEAD><TITLE></TITLE>
<META name=Application-State content=service=IDP;federated=False;env=Test;state=Token;authenticated=True;></HEAD>
<BODY SSO_SAML2_TOKEN=\"PHNhbWxwOlJlc3BvbnNlIElEPSJf [Truncated] VzcG9uc2U+\"></BODY>


In such a case SAML response can be read directly from the SSO_SAML2_TOKEN attribute on the BODY element. The response is URLEncoded and Base64 encoded and after decoding contains the SAML Response which looks something like this:

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" ).

  • 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 exactly identical. It also needs appKey and appsecret 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:

As shown in the sequence diagram above, the application flow is very similar to the Web Server application login flow. The only difference here is it assumes that the application has access to a system browser, or has a browser embedded as a web view. Installed application can keep client secret and key in the application code or can talk to a back end Server application for getting the access token. In case the application is storing the application secret and key in application code then its the responsibility of the application to take proper measure to ensure that nobody can spoof the app secret and key.

Downloads

 Download Sample Application