The purpose of authentication and authorization is to deny public access to private resources and minimize the possibility of access leak.
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.
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.
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 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.
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
{
"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
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
{
"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
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%
{
"access_token": "f99b9f57-cf69-4715-ad42-5d6c92d23e06",
"expires_in": 10079,
"refresh_token": "3b4d5a82-bc3d-4b19-a188-2721f7171a45",
"scope": "public",
"token_type": "bearer"
}
const client = Stomp.over(websocket);
client.connect({ 'Authorization': 'f99b9f57-cf69-4715-ad42-5d6c92d23e06' }, () => { /* connected */})
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.
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.
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.
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.
Class that defines a body of any error.
| 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. |
{
"message" : "User record with specified username already exists.",
"status_code" : "DUPLICATED_USERNAME"
}
{
"message": "Nonce.",
"status_code": null
}
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)