Authentication

The purpose of authentication and authorization is to deny public access to private resources and minimize the possibility of access leak.

Tokens

Our security model uses two types of tokens: access token and refresh token. This is the default security model for Spring MVC.

Access token is used to sign each REST API request and websocket STOMP CONNECT request. Requests without access token or with expired access token will be denied with AccessDenied error code. Access token is usually valid only for a short period of time, so that if someone who has stolen it could not use it for a long period of time.

Refresh token is used for retrieving new access token, once previous was expired. Refresh token is usually valid for a long period of time, since it cannot be updated. Expiration of the refresh token will result in user logout.

Since websocket works over a single TCP connection we suppose that it is secured enough and it’s messages don’t need to be signed, once the first authentication succeeds. That is why we only require STOMP CONNECT request to be signed by a valid access token. This request is sent each time a new websocket connection is established, so on reconnecting you need to make sure you have a valid access token, and update it using refresh token if needed.

Two-Factor Authentication

Our backend supports two-factor authentication (2FA), using Google Authenticator. Each user can enable 2FA for himself personally. During enabling procedure, user will receive a 16-symbol secret code, which is used by Google Authenticator to generate verification codes. Verification code is used to verify user during login. Each verification code is only valid for 30 seconds.

If the user has enabled 2FA, normal login attempt will return with 401 (Unauthorized) status code and text Verification code required. In this case you’ll need to repeat authentication attempt with additional body field code, containing verification code, generated by Google Authenticator. In case verification code is outdated or invalid 401 (Unauthorized) status code will return with text Invalid verification code.. All other authentication steps remain the same.

Usage

REST API

Each private REST API endpoint requires authentication token and token type. To use: add combination of token type (token_type) and access token (access_token) to request headers. For example:

authorization: bearer f99b9f57-cf69-4715-ad42-5d6c92d23e06

Websocket

Websocket requires authentication token after connection. To use: add authentication token (access_token) for CONNECT STOMP message to headers. For example:

CONNECT
Authorization:f99b9f57-cf69-4715-ad42-5d6c92d23e06
accept-version:1.1,1.0
heart-beat:10000,10000

Field accept-version is required by STOP specification. Supported versions are 1.1 and 1.2.

Field heart-beat specifies outgoing and incoming heart-beats rate (the smallest number of milliseconds between heart-beats). If this is not specified, websocket connection can be disconnected if no outgoing/incoming messages are send during a significant amount of time. 10000 is default and it is advisable not to set timeouts less than that.

If Access Denied error is received, client should update access token, using provided refresh token and recreate websocket connection, since any STOMP ERROR breaks the connection.

HTTP Requests

Get authentication token:

Request:

Url: /oauth/token
Method: POST
Headers:

authorization: Basic d2ViOg==
content-Type: application/x-www-form-urlencoded

Body:

username: %USER_NAME%
password: %USER_PASSWORD%
grant_type: password
scope: public

Response example

{
    "access_token": "f99b9f57-cf69-4715-ad42-5d6c92d23e06",
    "expires_in": 10079,
    "refresh_token": "3b4d5a82-bc3d-4b19-a188-2721f7171a45",
    "scope": "public",
    "token_type": "bearer"
}

where:
access_token - private user authentication token
expires_in - expiration time of token (in seconds)
refresh_token - private user token to renew access_token
scope - client limitation group (public by default)
token_type - type of token

Get authentication token for with verification code:

Request:

Url: /oauth/token
Method: POST
Headers:

authorization: Basic d2ViOg==
content-Type: application/x-www-form-urlencoded

Body:

username: %USER_NAME%
password: %USER_PASSWORD%
grant_type: password
scope: public
code: 123456

Response example

{
    "access_token": "f99b9f57-cf69-4715-ad42-5d6c92d23e06",
    "expires_in": 10079,
    "refresh_token": "3b4d5a82-bc3d-4b19-a188-2721f7171a45",
    "scope": "public",
    "token_type": "bearer"
}

where:
access_token - private user authentication token
expires_in - expiration time of token (in seconds)
refresh_token - private user token to renew access_token
scope - client limitation group (public by default)
token_type - type of token

Refresh authentication token:

Request:

Url: /oauth/token
Method: POST
Headers:

authorization: Basic d2ViOg==
content-Type: application/x-www-form-urlencoded

Body:

grant_type: refresh_token
refresh_token: %REFRESH_TOKEN%

Response example

{
    "access_token": "f99b9f57-cf69-4715-ad42-5d6c92d23e06",
    "expires_in": 10079,
    "refresh_token": "3b4d5a82-bc3d-4b19-a188-2721f7171a45",
    "scope": "public",
    "token_type": "bearer"
}

JS example

const client = Stomp.over(websocket);
client.connect({ 'Authorization': 'f99b9f57-cf69-4715-ad42-5d6c92d23e06' }, () => { /* connected */})

Keep Alive

For each logged-in user we store some resources in memory. Since total amount of users can be large enough so that their aggregated data cannot fit into available memory, we need to free allocated resources for inactive users.

In order to release resources, acquired by user login, we use keep alive mechanism. We remember last timestamp of each REST or websocket request. When any logged-in session doesn’t receive any message for a period of time longer than keepalive timeout, we consider such session inactive and suggest that it’s resources can be released.

In case, client doesn’t need to send any informative REST requests, we expose a special utility /keepalive request to inform backend that current session is still alive. This request returns backend’s configured keepalive timeout. In websocket session is kept alive by STOMP HEARTBEAT messages. Since default timeout for HEARTBEAT websocket messages is 10 seconds it is strongly advised not to set keepalive timeout less then 10 seconds.

Why not use refresh token validity time for this purpose? On the one hand, we don’t want to logout active users too often, so refresh token timeout is usually wanted to be large - at least several days, or even months. On the other hand, we don’t want to keep user’s data in memory during such long period of time. Usually logouting user that left several hours or even minutes ago won’t have any negative effects.

Nonce

In order to increase security and prevent the same message to be resent repeatedly, most of our requests require a special header X-Deltix-Nonce to be set. This is a numeric value, called nonce. Within each websocket session and each REST session, identified by the access token, each subsequent request must have nonce value greater than the previous one. If the request contains the same or lower nonce as previous one, such request will be rejected with status 400 (Bad Request) and message "Nonce.".

Recommended value for nonce is timestamp with at least milliseconds resolution. This may still be not enough thou if you send requests too frequently. In this case you’ll have to manage nonce manually and increment it with each request sent.

Known problems

Problem: If you send several REST requests simultaneously (any REST requests, regardless difference in URLs, headers etc.) they may come to backend in different order, depending on your configuration, which will lead to failure for some of them because of nonce.

Solution: You may send requests consistently. If this is not the option - you may switch to websocket API. Almost all requests that require nonce may be send via websocket and it guarantees message order.

Problem: If you use current timestamp with milliseconds resolution as nonce, some requests sent one after another may have the same nonce, which will lead to failure for the later ones.

Solution: You may store current nonce as a field, initialize it with current timestamp and then increment it with each new request.

Errors

Request errors may occur on different stages of request processing. Error responses may have different structures depending on the source of error - whether it is nginx or any other web proxy, or Spring MVC. If an error happens inside our code, response will have message ErrorDto as body.

ErrorDto

Class that defines a body of any error.

Properties

name data type description
status_code string Optional implementation-defined error status code that can help understand the reason of the error and, possibly, process it automatically.
message string Human-readable error description.

Examples

{
  "message" : "User record with specified username already exists.",
  "status_code" : "DUPLICATED_USERNAME"
}
{
    "message": "Nonce.",
    "status_code": null
}

Complete Examples

Login and subscribe to market data and securities statistics using JS

require('isomorphic-fetch');
const stompjs = require("stompjs");

const getToken = (username, password, schema, host) => {
    const body = `username=${encodeURIComponent(username)}&password=${encodeURIComponent(password)}&grant_type=password&scope=public`;

    return fetch(`${schema}://${host}/oauth/token`, {
        headers: {
            "Authorization": "Basic d2ViOg==",
            "Content-Type": "application/x-www-form-urlencoded",
        },
        method: "POST",
        body,
    })
    .then(resp => resp.json())
    .then(({access_token}) => {
        if (undefined === access_token) {
            throw new Error("Authentication failed.");
        }
        console.log(access_token);

        return access_token;
    });
};

const connect = (schema, host, apiVersion) => token => {
    const url = `${schema}://${host}/websocket/${apiVersion}`;

    const client = stompjs.overWS(url);

    return new Promise((resolve, reject) => {
        client.connect(
            { Authorization: `${token}` },
            () => resolve(client),
            reject
        );
    });
};

const subscribeToL2 = client => client
    .subscribe(
        `/user/v1/market-data/BTCUSD`,
        ({body}) => console.log(body)
        , {"X-Deltix-Nonce": Date.now() + 1,}
    );

const subscribeToStats = client => client
    .subscribe(
        `/user/v1/securities/statistics`,
        ({body}) => console.log(body)
        , {"X-Deltix-Nonce": Date.now(),}
    );

const username = "Ava Parsons"
const password = "password";
const host = "127.0.0.1";
const apiVersion = 'v1';

getToken(username, password, "http", host)
    .then(connect("ws", host, apiVersion))
    .then(client => {
        subscribeToStats(client);
        subscribeToL2(client);
    })
    .catch(console.error)