Skip to content
This repository was archived by the owner on Mar 11, 2022. It is now read-only.

Commit 13d957b

Browse files
committed
Add new TokenManager class.
1 parent a38de10 commit 13d957b

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

lib/tokens/TokenManager.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Copyright © 2019 IBM Corp. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
'use strict';
15+
16+
const debug = require('debug')('cloudant:tokens:tokenmanager');
17+
const cookie = require('cookie');
18+
const EventEmitter = require('events');
19+
20+
class TokenManager {
21+
constructor(client, jar, sessionUrl) {
22+
this._client = client;
23+
this._jar = jar;
24+
this._sessionUrl = sessionUrl;
25+
26+
this._attemptTokenRenewal = true;
27+
this._isTokenRenewing = false;
28+
29+
this._tokenExchangeEE = new EventEmitter().setMaxListeners(Infinity);
30+
}
31+
32+
_autoRenew() {
33+
debug('Auto renewing token now...');
34+
this._renew().then((response) => {
35+
let maxAgeSecs = cookie.parse(response.headers['set-cookie'][0])['Max-Age'] || 3600000;
36+
let delaySecs = maxAgeSecs / 2;
37+
debug(`Renewing token in ${delaySecs} seconds.`);
38+
setTimeout(this._autoRenew.bind(this), delaySecs * 1000).unref();
39+
}).catch((error) => {
40+
debug(`Failed to auto renew token - ${error}. Retrying in 60 seconds.`);
41+
setTimeout(this._autoRenew.bind(this), 60000).unref();
42+
});
43+
}
44+
45+
_getToken(callback) {
46+
// ** Method to be implemented by _all_ subclasses of `TokenManager` **
47+
throw new Error('Not implemented.');
48+
}
49+
50+
// Renew the token.
51+
_renew() {
52+
if (!this._isTokenRenewing) {
53+
this._isTokenRenewing = true;
54+
this._tokenExchangeEE.removeAllListeners();
55+
debug('Starting token renewal.');
56+
this._getToken((error, response) => {
57+
if (error) {
58+
this._tokenExchangeEE.emit('error', error, response);
59+
} else {
60+
this._tokenExchangeEE.emit('success', response);
61+
this._attemptTokenRenewal = false;
62+
}
63+
debug('Finished token renewal.');
64+
this._isTokenRenewing = false;
65+
});
66+
}
67+
return new Promise((resolve, reject) => {
68+
this._tokenExchangeEE.once('success', resolve);
69+
this._tokenExchangeEE.once('error', (error, response) => {
70+
error.response = response;
71+
reject(error);
72+
});
73+
});
74+
}
75+
76+
// Getter for `attemptTokenRenewal`.
77+
// - `true` A renewal attempt will be made on the next `renew` request.
78+
// - `false` No renewal attempt will be made on the next `renew`
79+
// request. Instead the last good renewal response will be returned
80+
// to the client.
81+
get attemptTokenRenewal() {
82+
return this._attemptTokenRenewal;
83+
}
84+
85+
// Getter for `isTokenRenewing`.
86+
// - `true` A renewal attempt is in progress.
87+
// - `false` There are no in progress renewal attempts.
88+
get isTokenRenewing() {
89+
return this._isTokenRenewing;
90+
}
91+
92+
// Settter for `attemptTokenRenewal`.
93+
set attemptTokenRenewal(newAttemptTokenRenewal) {
94+
this._attemptTokenRenewal = newAttemptTokenRenewal;
95+
}
96+
97+
// Renew the token if `attemptTokenRenewal` is `true`. Otherwise this is a
98+
// no-op so just resolve the promise.
99+
renewIfRequired() {
100+
if (this._attemptTokenRenewal) {
101+
return this._renew();
102+
} else {
103+
return new Promise((resolve) => {
104+
resolve();
105+
});
106+
}
107+
}
108+
109+
// Start the auto renewal timer.
110+
startAutoRenew() {
111+
this._autoRenew();
112+
}
113+
}
114+
115+
module.exports = TokenManager;

package-lock.json

Lines changed: 5 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
"@types/request": "^2.47.0",
2727
"async": "2.1.2",
2828
"concat-stream": "^1.6.0",
29+
"cookie": "^0.4.0",
2930
"debug": "^3.1.0",
3031
"lockfile": "1.0.3",
3132
"nano": "^8.1.0",

test/tokens/TokenManager.js

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright © 2019 IBM Corp. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
/* global describe it */
16+
'use strict';
17+
18+
const assert = require('assert');
19+
20+
const TokenManager = require('../../lib/tokens/TokenManager');
21+
22+
class TokenManagerRenewSuccess extends TokenManager {
23+
constructor() {
24+
super();
25+
this._getTokenCallCount = 0;
26+
}
27+
28+
_getToken(callback) {
29+
this._getTokenCallCount += 1;
30+
setTimeout(() => {
31+
callback(null, { headers: { 'set-cookie': [ 'Max-Age=1' ] } });
32+
}, 100);
33+
}
34+
35+
// mock successful token renewal
36+
get getTokenCallCount() {
37+
return this._getTokenCallCount;
38+
}
39+
}
40+
41+
class TokenManagerRenewFailure extends TokenManager {
42+
constructor() {
43+
super();
44+
this._getTokenCallCount = 0;
45+
}
46+
47+
// mock failed token renewal
48+
_getToken(callback) {
49+
this._getTokenCallCount += 1;
50+
setTimeout(() => {
51+
callback(new Error('err'), {ok: false});
52+
}, 100);
53+
}
54+
55+
get getTokenCallCount() {
56+
return this._getTokenCallCount;
57+
}
58+
}
59+
60+
describe('Token Manger', (done) => {
61+
it('renews the token successfully', (done) => {
62+
let t = new TokenManagerRenewSuccess();
63+
t.renewIfRequired().then(() => {
64+
assert.equal(t.getTokenCallCount, 1);
65+
done();
66+
}).catch(done);
67+
assert.ok(t.isTokenRenewing);
68+
});
69+
70+
it('handles a token renewal failure', (done) => {
71+
let t = new TokenManagerRenewFailure();
72+
t.renewIfRequired().then(() => {
73+
assert.fail('Unexpected success.');
74+
}).catch((error) => {
75+
assert.equal(t.getTokenCallCount, 1);
76+
assert.equal(error.message, 'err');
77+
assert.equal(error.response.ok, false);
78+
done();
79+
});
80+
assert.ok(t.isTokenRenewing);
81+
});
82+
83+
it('correctly auto renews token', (done) => {
84+
let t = new TokenManagerRenewSuccess();
85+
t.startAutoRenew();
86+
setTimeout(() => {
87+
// one renew every 0.5 seconds
88+
assert.equal(t.getTokenCallCount, 4);
89+
done();
90+
}, 2000);
91+
});
92+
93+
it('only makes one renewal request', (done) => {
94+
let t = new TokenManagerRenewSuccess();
95+
let renewalCount = 0;
96+
let lim = 10000;
97+
98+
for (let i = 1; i < lim + 1; i++) {
99+
if (i === lim) {
100+
t.renewIfRequired().then(() => {
101+
renewalCount += 1;
102+
assert.equal(renewalCount, lim);
103+
assert.equal(t.getTokenCallCount, 1);
104+
done();
105+
}).catch(done);
106+
} else {
107+
t.renewIfRequired().then(() => {
108+
renewalCount += 1;
109+
}).catch(done);
110+
}
111+
}
112+
assert.ok(t.isTokenRenewing);
113+
});
114+
115+
it('makes another renewal only after setting force renew', (done) => {
116+
let t = new TokenManagerRenewSuccess();
117+
// renew 1 - make request
118+
t.renewIfRequired().then(() => {
119+
assert.equal(t.getTokenCallCount, 1);
120+
// renew 2 - return last good response
121+
t.renewIfRequired().then(() => {
122+
assert.equal(t.getTokenCallCount, 1);
123+
t.attemptTokenRenewal = true;
124+
// renew 3 - make request
125+
t.renewIfRequired().then(() => {
126+
assert.equal(t.getTokenCallCount, 2);
127+
done();
128+
});
129+
});
130+
}).catch(done);
131+
assert.ok(t.isTokenRenewing);
132+
});
133+
});

0 commit comments

Comments
 (0)