Introduction
This section explains the login & authentication flow of web-based application.
Overall, the login process consists of 2 steps:
Authentication - Single SignOn
- 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:
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. | http://www.mytestsite.com/ |
AuthenticationUrl | The URL of the Saxo Bank authentication & authorization server - this was provided to you when you registered | https://www.logonvalidation.net/ |
AppKey | The Application key identifying your application to Saxo Bank - this was provided to you when you registered | - |
AppSecret | The Application "secret" identifying your application to Saxo Bank - this was provided to you when you registered | - |
OpenApiBaseUrl | Base 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:
<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:
Field | Description | line no. in example |
---|---|---|
ID | A unique ID - in the example above it is a guid preceded by a "_" | 1 |
IssueInstant | The 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:
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):
<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: