Skip to content

Commit c775313

Browse files
committed
Add support for JWT-based authentication (by passing a function that "provides" the JWT to Connection.connect)
1 parent 214e1ce commit c775313

File tree

4 files changed

+188
-30
lines changed

4 files changed

+188
-30
lines changed

docs/content/releases/6.3.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
**New Features**
2+
3+
* Added support for authentication using [JWT](https://en.wikipedia.org/wiki/JSON_Web_Token). This feature is intentionally undocumented.

lib/connection/.meta.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,17 @@
1010
* @namespace Callbacks
1111
*/
1212

13+
/**
14+
* The signature of a function which returns a JWT. The JWT is
15+
* then used as an alternative for credentials (i.e. a username
16+
* and password). The function can be synchronous or asynchronous.
17+
*
18+
* @public
19+
* @callback JwtProvider
20+
* @memberOf Callbacks
21+
* @returns {string|Promise<string>}
22+
*/
23+
1324
/**
1425
* The signature of a function which accepts events generated by an
1526
* **Events** subscription (see {@link Enums.SubscriptionType}).

lib/connection/Connection.js

Lines changed: 149 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const array = require('@barchart/common-js/lang/array'),
2+
assert = require('@barchart/common-js/lang/assert'),
23
object = require('@barchart/common-js/lang/object');
34

45
const ConnectionBase = require('./ConnectionBase'),
@@ -21,6 +22,11 @@ module.exports = (() => {
2122

2223
const _API_VERSION = 4;
2324

25+
const mode = {
26+
credentials: 'credentials',
27+
token: 'token'
28+
};
29+
2430
const state = {
2531
connecting: 'CONNECTING',
2632
authenticating: 'LOGGING_IN',
@@ -46,6 +52,9 @@ module.exports = (() => {
4652
const _RECONNECT_INTERVAL = 5000;
4753
const _WATCHDOG_INTERVAL = 10000;
4854

55+
const regex = { };
56+
regex.hostname = /^(?:(wss|ws):\/\/)?(.+?)(?::(\d+))?$/i;
57+
4958
function ConnectionInternal(marketState, instance) {
5059
const __logger = LoggerFactory.getLogger('@barchart/marketdata-api-js');
5160

@@ -60,6 +69,7 @@ module.exports = (() => {
6069

6170
let __connection = null;
6271
let __connectionState = state.disconnected;
72+
let __connectionCount = 0;
6373

6474
let __paused = false;
6575

@@ -94,9 +104,12 @@ module.exports = (() => {
94104
};
95105

96106
const __loginInfo = {
107+
mode: null,
97108
hostname: null,
98109
username: null,
99-
password: null
110+
password: null,
111+
jwtProvider: null,
112+
jwtPromise: null
100113
};
101114

102115
let __decoder = null;
@@ -154,18 +167,19 @@ module.exports = (() => {
154167
*
155168
* @private
156169
* @param {String} hostname
157-
* @param {String} username
158-
* @param {String} password
170+
* @param {String=} username
171+
* @param {String=} password
159172
* @param {WebSocketAdapterFactory} webSocketAdapterFactory
160173
* @param {XmlParserFactory} xmlParserFactory
174+
* @param {Callbacks.JwtProvider} jwtProvider
161175
*/
162-
function initializeConnection(hostname, username, password, webSocketAdapterFactory, xmlParserFactory) {
176+
function initializeConnection(hostname, username, password, webSocketAdapterFactory, xmlParserFactory, jwtProvider) {
163177
__connectionFactory = webSocketAdapterFactory;
164178
__xmlParserFactory = xmlParserFactory;
165179

166180
__reconnectAllowed = true;
167181

168-
connect(hostname, username, password);
182+
connect(hostname, username, password, jwtProvider);
169183
}
170184

171185
/**
@@ -179,9 +193,12 @@ module.exports = (() => {
179193

180194
__reconnectAllowed = false;
181195

196+
__loginInfo.mode = null;
197+
__loginInfo.hostname = null;
182198
__loginInfo.username = null;
183199
__loginInfo.password = null;
184-
__loginInfo.hostname = null;
200+
__loginInfo.jwtProvider = null;
201+
__loginInfo.jwtPromise = null;
185202

186203
__knownConsumerSymbols = {};
187204
__pendingProfileSymbols = {};
@@ -197,24 +214,62 @@ module.exports = (() => {
197214
disconnect();
198215
}
199216

217+
/**
218+
* Attempts to read a JWT from an external provider.
219+
*
220+
* @private
221+
* @param {Callbacks.JwtProvider} jwtProvider
222+
* @returns {Promise<string|null>}
223+
*/
224+
function getJwt(jwtProvider) {
225+
const connectionCount = __connectionCount;
226+
227+
return Promise.resolve()
228+
.then(() => {
229+
__logger.log(`Connection [ ${__instance} ]: Requesting JWT for connection attempt [ ${connectionCount} ].`);
230+
231+
return jwtProvider();
232+
}).then((jwt) => {
233+
__logger.log(`Connection [ ${__instance} ]: Request for JWT was successful for connection attempt [ ${connectionCount} ].`);
234+
235+
if (__connectionCount !== connectionCount) {
236+
return null;
237+
}
238+
239+
if (typeof(jwt) !== 'string') {
240+
__logger.warn(`Connection [ ${__instance} ]: Unable to extract JWT.`);
241+
242+
return null;
243+
}
244+
245+
return jwt;
246+
}).catch((e) => {
247+
__logger.warn(`Connection [ ${__instance} ]: Request for JWT failed for connection attempt [ ${connectionCount} ].`);
248+
249+
return null;
250+
});
251+
}
252+
200253
/**
201254
* Attempts to establish a connection to JERQ.
202255
*
203256
* @private
204257
* @param {String} hostname
205-
* @param {String} username
206-
* @param {String} password
258+
* @param {String=} username
259+
* @param {String=} password
260+
* @param {Callbacks.JwtProvider=} jwtProvider
207261
*/
208-
function connect(hostname, username, password) {
209-
if (!hostname) {
210-
throw new Error('Unable to connect, the "hostname" argument is required.');
211-
}
262+
function connect(hostname, username, password, jwtProvider) {
263+
assert.argumentIsRequired(hostname, 'hostname', String);
264+
assert.argumentIsOptional(username, 'username', String);
265+
assert.argumentIsOptional(password, 'password', String);
266+
assert.argumentIsOptional(jwtProvider, 'jwtProvider', Function);
212267

213-
if (!username) {
268+
if (!username && !jwtProvider) {
214269
throw new Error('Unable to connect, the "username" argument is required.');
215270
}
216271

217-
if (!password) {
272+
if (!password && !jwtProvider) {
218273
throw new Error('Unable to connect, the "password" argument is required.');
219274
}
220275

@@ -226,28 +281,67 @@ module.exports = (() => {
226281

227282
ensureExchangeMetadata();
228283

229-
__logger.log(`Connection [ ${__instance} ]: Initializing. Version [ ${version} ]. Using [ ${username}@${hostname} ].`);
284+
__connectionCount = __connectionCount + 1;
230285

231-
__xmlParser = __xmlParserFactory.build();
286+
__logger.log(`Connection [ ${__instance} ]: Starting connection attempt [ ${__connectionCount} ] using [ ${jwtProvider ? 'JWT' : 'credentials-based' } ] authentication.`);
232287

233288
let protocol;
289+
let host;
234290
let port;
235291

236292
if (hostname === 'localhost') {
237293
protocol = 'ws';
294+
host = 'localhost';
238295
port = 8080;
239296
} else {
240-
protocol = 'wss';
241-
port = 443;
297+
const match = hostname.match(regex.hostname);
298+
299+
if (match !== null && match[1]) {
300+
protocol = match[1];
301+
} else {
302+
protocol = 'wss';
303+
}
304+
305+
if (match !== null && match[2]) {
306+
host = match[2];
307+
} else {
308+
host = hostname;
309+
}
310+
311+
if (match !== null && match[3]) {
312+
port = parseInt(match[3]);
313+
} else {
314+
port = protocol === 'ws' ? 80 : 443;
315+
}
242316
}
243317

244-
__loginInfo.username = username;
245-
__loginInfo.password = password;
246318
__loginInfo.hostname = hostname;
247319

320+
__loginInfo.username = null;
321+
__loginInfo.password = null;
322+
323+
__loginInfo.jwtProvider = null;
324+
__loginInfo.jwtPromise = null;
325+
326+
if (jwtProvider) {
327+
__loginInfo.mode = mode.token;
328+
329+
__loginInfo.jwtProvider = jwtProvider;
330+
__loginInfo.jwtPromise = getJwt(jwtProvider);
331+
} else {
332+
__loginInfo.mode = mode.credentials;
333+
334+
__loginInfo.username = username;
335+
__loginInfo.password = password;
336+
}
337+
338+
__xmlParser = __xmlParserFactory.build();
339+
248340
__connectionState = state.disconnected;
249341

250-
__connection = __connectionFactory.build(`${protocol}://${__loginInfo.hostname}:${port}/jerq`);
342+
__logger.log(`Connection [ ${__instance} ]: Opening connection to [ ${protocol}://${host}:${port} ].`);
343+
344+
__connection = __connectionFactory.build(`${protocol}://${host}:${port}/jerq`);
251345
__connection.binaryType = 'arraybuffer';
252346

253347
__decoder = __connection.getDecoder();
@@ -307,7 +401,7 @@ module.exports = (() => {
307401
if (__reconnectAllowed) {
308402
__logger.log(`Connection [ ${__instance} ]: Scheduling reconnect attempt.`);
309403

310-
const reconnectAction = () => connect(__loginInfo.hostname, __loginInfo.username, __loginInfo.password);
404+
const reconnectAction = () => connect(__loginInfo.hostname, __loginInfo.username, __loginInfo.password, __loginInfo.jwtProvider);
311405
const reconnectDelay = _RECONNECT_INTERVAL + Math.floor(Math.random() * _WATCHDOG_INTERVAL);
312406

313407
setTimeout(reconnectAction, reconnectDelay);
@@ -836,9 +930,39 @@ module.exports = (() => {
836930
if (lines.some(line => line === '+++')) {
837931
__connectionState = state.authenticating;
838932

839-
__logger.log(`Connection [ ${__instance} ]: Sending credentials.`);
933+
let connectionCount = __connectionCount;
840934

841-
__connection.send(`LOGIN ${__loginInfo.username}:${__loginInfo.password} VERSION=${_API_VERSION}\r\n`);
935+
if (__loginInfo.mode === mode.credentials) {
936+
__connection.send(`LOGIN ${__loginInfo.username}:${__loginInfo.password} VERSION=${_API_VERSION}\r\n`);
937+
938+
return;
939+
}
940+
941+
if (__loginInfo.mode === mode.token) {
942+
const jwtPromise = __loginInfo.jwtPromise || Promise.resolve(null);
943+
944+
jwtPromise.then((jwt) => {
945+
if (__connectionCount !== connectionCount) {
946+
return;
947+
}
948+
949+
if (__connectionState !== state.authenticating) {
950+
return;
951+
}
952+
953+
if (jwt === null) {
954+
broadcastEvent('events', { event: 'jwt acquisition failed' });
955+
956+
disconnect();
957+
958+
return;
959+
}
960+
961+
__connection.send(`TOKEN ${jwt} VERSION=${_API_VERSION}\r\n`);
962+
});
963+
964+
return;
965+
}
842966
}
843967
} else if (__connectionState === state.authenticating) {
844968
const lines = message.split('\n');
@@ -1847,7 +1971,7 @@ module.exports = (() => {
18471971
}
18481972

18491973
_connect(webSocketAdapterFactory, xmlParserFactory) {
1850-
this._internal.connect(this.getHostname(), this.getUsername(), this.getPassword(), webSocketAdapterFactory, xmlParserFactory);
1974+
this._internal.connect(this.getHostname(), this.getUsername(), this.getPassword(), webSocketAdapterFactory, xmlParserFactory, this.getJwtProvider());
18511975
}
18521976

18531977
_disconnect() {

lib/connection/ConnectionBase.js

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,12 @@ module.exports = (() => {
2222
class ConnectionBase {
2323
constructor(environment) {
2424
this._hostname = null;
25+
2526
this._username = null;
2627
this._password = null;
2728

29+
this._jwtProvider = null;
30+
2831
this._environment = environment || new EnvironmentForBrowsers();
2932
this._marketState = new MarketState(symbol => this._handleProfileRequest(symbol));
3033

@@ -44,15 +47,19 @@ module.exports = (() => {
4447
*
4548
* @public
4649
* @param {string} hostname - Barchart hostname (contact [email protected])
47-
* @param {string} username - Your username (contact [email protected])
48-
* @param {string} password - Your password (contact [email protected])
50+
* @param {string=} username - Your username (contact [email protected])
51+
* @param {string=} password - Your password (contact [email protected])
4952
* @param {WebSocketAdapterFactory=} webSocketAdapterFactory - Strategy for creating a {@link WebSocketAdapterFactory} instances (overrides {@link Environment} settings).
5053
* @param {XmlParserFactory=} xmlParserFactory - Strategy for creating a {@link WebSocketAdapterFactory} instances (overrides {@link Environment} settings).
54+
* @param {Callbacks.JwtProvider=} jwtProvider - A function which returns a JWT (or a promise for a JWT) that is used as an alternative for actual credentials.
5155
*/
52-
connect(hostname, username, password, webSocketAdapterFactory, xmlParserFactory) {
56+
connect(hostname, username, password, webSocketAdapterFactory, xmlParserFactory, jwtProvider) {
5357
this._hostname = hostname;
54-
this._username = username;
55-
this._password = password;
58+
59+
this._username = username || null;
60+
this._password = password || null;
61+
62+
this._jwtProvider = jwtProvider || null;
5663

5764
this._connect(webSocketAdapterFactory || this._environment.getWebSocketAdapterFactory(), xmlParserFactory || this._environment.getXmlParserFactory());
5865
}
@@ -75,9 +82,12 @@ module.exports = (() => {
7582
*/
7683
disconnect() {
7784
this._hostname = null;
85+
7886
this._username = null;
7987
this._password = null;
8088

89+
this._jwtProvider = null;
90+
8191
this._disconnect();
8292
}
8393

@@ -369,6 +379,16 @@ module.exports = (() => {
369379
return this._username;
370380
}
371381

382+
/**
383+
* The username used to authenticate to Barchart.
384+
*
385+
* @public
386+
* @returns {null|Callbacks.JwtProvider}
387+
*/
388+
getJwtProvider() {
389+
return this._jwtProvider;
390+
}
391+
372392
/**
373393
* Gets a unique identifier for the current instance.
374394
*

0 commit comments

Comments
 (0)