node-vault-client 3.0.0 → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -13
- package/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/auth/AuthToken.js +88 -23
- package/src/auth/VaultBaseAuth.js +143 -4
package/README.md
CHANGED
|
@@ -365,19 +365,31 @@ on the next call — no background timer at all. Two reasons to want that:
|
|
|
365
365
|
Renewal off means the token is allowed to expire, and what happens next depends on whether your
|
|
366
366
|
backend can obtain a *fresh* credential unaided:
|
|
367
367
|
|
|
368
|
-
| backend | on expiry with `renewal: false` |
|
|
369
|
-
| --- | --- |
|
|
370
|
-
| `kubernetes`, `iam`, `jwt` with `jwtPath`/`jwtProvider` | clean re-login — the JWT or AWS credential is re-acquired, so this is the intended case |
|
|
371
|
-
| `appRole` | re-login **replays the same `secret_id`**. Fine for a reusable one; with Vault's recommended hardening (`secret_id_num_uses=1`, or a short `secret_id_ttl`) the second login is rejected |
|
|
372
|
-
| `jwt` with a literal `jwt` | replays the same JWT, so it works only until the IdP-issued token expires |
|
|
373
|
-
| `token` | **cannot re-authenticate at all.** The client raises `AuthTokenExpiredError` from then on, permanently |
|
|
368
|
+
| backend | on expiry with `renewal: false` | at max TTL with renewal enabled |
|
|
369
|
+
| --- | --- | --- |
|
|
370
|
+
| `kubernetes`, `iam`, `jwt` with `jwtPath`/`jwtProvider` | clean re-login — the JWT or AWS credential is re-acquired, so this is the intended case | clean re-login on the next call after the client-side expiry deadline |
|
|
371
|
+
| `appRole` | re-login **replays the same `secret_id`**. Fine for a reusable one; with Vault's recommended hardening (`secret_id_num_uses=1`, or a short `secret_id_ttl`) the second login is rejected | re-login replays the same `secret_id`; a single-use or expired secret is rejected |
|
|
372
|
+
| `jwt` with a literal `jwt` | replays the same JWT, so it works only until the IdP-issued token expires | replays the same JWT; once it expires, login is rejected |
|
|
373
|
+
| `token` | **cannot re-authenticate at all.** The client raises `AuthTokenExpiredError` from then on, permanently | **cannot re-authenticate at all.** The client raises `AuthTokenExpiredError` after the max-TTL expiry |
|
|
374
|
+
|
|
375
|
+
#### Tokens that reach their maximum TTL
|
|
376
|
+
|
|
377
|
+
Vault stops extending a renewable token when it reaches its maximum TTL. The client uses Vault's
|
|
378
|
+
absolute `expire_time` when available, treats `ttl: 0` as expired immediately, and stops background
|
|
379
|
+
renewal when successive absolute expiry deadlines do not materially advance. Because Vault reports
|
|
380
|
+
whole-second TTLs, the scheduler also compares the reported TTL with the elapsed renewal interval;
|
|
381
|
+
short periodic tokens keep renewing even when each expiry advance is one second or less. A
|
|
382
|
+
`renewalIncrement` that meaningfully shortens the expiry still schedules from the new deadline. For positive TTLs above 60 seconds, the client keeps a one-minute
|
|
383
|
+
safety margin for requests in flight; TTLs of 60 seconds or less have no margin. The next call after
|
|
384
|
+
the client-side expiry deadline re-authenticates, as shown in the table above.
|
|
374
385
|
|
|
375
386
|
Two consequences worth stating plainly:
|
|
376
387
|
|
|
377
|
-
* For `token` auth
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
`
|
|
388
|
+
* **For `token` auth, there is no fresh credential for re-authentication.** With `renewal: false`,
|
|
389
|
+
an otherwise renewable token is allowed to expire without a background renewal. With renewal
|
|
390
|
+
enabled, the client stops the background loop once Vault no longer extends the expiry materially.
|
|
391
|
+
After expiry every later call rejects for the life of the process. `close()` does not reset it —
|
|
392
|
+
recovery means `VaultClient.clear(name)` and booting again.
|
|
381
393
|
* For `appRole` and literal-`jwt`, a login that can no longer succeed is retried on **every**
|
|
382
394
|
subsequent call, since a failed login clears the cached token. That is a failing request per
|
|
383
395
|
`read()`, with no backoff. Prefer `jwtPath`/`jwtProvider`, a reusable `secret_id`, or leaving
|
|
@@ -410,8 +422,8 @@ auth: {
|
|
|
410
422
|
Lower `renewalFraction` values renew earlier and more often, which buys headroom if Vault is briefly
|
|
411
423
|
unreachable. A failed renewal is retried on the same rule against the *same* token, so the waits
|
|
412
424
|
shrink geometrically as the remaining lifetime does — for a 1-hour token that is roughly 12 attempts
|
|
413
|
-
at `0.5` (1800s, 900s, 450s, …) versus 27 at `0.25`, both bottoming out at a
|
|
414
|
-
before expiry. Higher values renew later and talk to Vault less.
|
|
425
|
+
at `0.5` (1800s, 900s, 450s, …) versus 27 at `0.25`, both bottoming out at a 250 ms minimum delay
|
|
426
|
+
when it still fits before expiry. Higher values renew later and talk to Vault less.
|
|
415
427
|
|
|
416
428
|
`renewalIncrement` is a request, not a guarantee: Vault grants at most the token's max TTL and may
|
|
417
429
|
return less. Both keys are validated at construction — a `renewalFraction` outside `(0, 1)` or a
|
|
@@ -1018,7 +1030,7 @@ try {
|
|
|
1018
1030
|
| `VaultHttpError` | `VaultError` | Vault answered with a non-2xx status. Carries the status as `statusCode` and the parsed body as `error`. `message` (`"<status> - <raw body text>"`) and `error` hold the full response body, which can include secret material (e.g. a partially failed transit batch), so log `statusCode` and `error.errors` rather than the error object itself. |
|
|
1019
1031
|
| `InvalidArgumentsError` | `VaultError` | Bad arguments or configuration: `boot()` called without options, an unknown instance name passed to `get()`, an unsupported `auth.type`, invalid renewal options, an invalid node-config substitution map, or a KV method path that fails [path validation](#path-validation). |
|
|
1020
1032
|
| `InvalidAWSCredentialsError` | `InvalidArgumentsError` | `auth.config.credentials` was supplied but is not a usable `accessKeyId` / `secretAccessKey` pair. |
|
|
1021
|
-
| `AuthTokenExpiredError` | `VaultError` | The Vault token expired and the backend cannot obtain a new one. `token` auth can never re-authenticate; other backends reach
|
|
1033
|
+
| `AuthTokenExpiredError` | `VaultError` | The Vault token expired and the backend cannot obtain a new one. `token` auth can never re-authenticate and reaches this error at max TTL even with renewal enabled; other backends can also reach it when re-authentication is disabled or their credential can no longer be replayed. |
|
|
1022
1034
|
| `UnsupportedOperationError` | `VaultError` | A v2-only method (`deleteVersions`, `undeleteVersions`, `destroyVersions`, `readMetadata`, `deleteMetadata`) was called against a mount that did not resolve as KV v2. |
|
|
1023
1035
|
|
|
1024
1036
|
## Contributing
|
package/index.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "node-vault-client",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.1",
|
|
4
4
|
"description": "A Vault Client implemented in pure javascript for HashiCorp Vault. It supports variety of Auth Backends and performs lease renewal for issued auth token.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"url": "git+https://github.com/namecheap/node-vault-client.git"
|
package/src/auth/AuthToken.js
CHANGED
|
@@ -12,6 +12,10 @@ class AuthToken {
|
|
|
12
12
|
* @param {int} explicitMaxTtl - in seconds
|
|
13
13
|
* @param {int} numUses
|
|
14
14
|
* @param {boolean} isRenewable
|
|
15
|
+
* @param {int|null} expiresAtMilliseconds - precise expiry time when available
|
|
16
|
+
* @param {int|null} ttl - remaining lifetime reported by Vault, when available
|
|
17
|
+
* @param {boolean} hasPreciseExpiry - true when expiry came from Vault's expire_time
|
|
18
|
+
* @param {int|null} unadjustedExpiresAtMilliseconds - Vault's expiry before the client safety margin
|
|
15
19
|
*/
|
|
16
20
|
constructor(
|
|
17
21
|
id,
|
|
@@ -20,35 +24,58 @@ class AuthToken {
|
|
|
20
24
|
expiresAt,
|
|
21
25
|
explicitMaxTtl,
|
|
22
26
|
numUses,
|
|
23
|
-
isRenewable
|
|
27
|
+
isRenewable,
|
|
28
|
+
expiresAtMilliseconds = null,
|
|
29
|
+
ttl = null,
|
|
30
|
+
hasPreciseExpiry = false,
|
|
31
|
+
unadjustedExpiresAtMilliseconds = null
|
|
24
32
|
) {
|
|
25
33
|
this.__id = id;
|
|
26
34
|
this.__accessor = accessor;
|
|
27
35
|
this.__createdAt = createdAt;
|
|
28
|
-
this.
|
|
36
|
+
this.__expiresAtMilliseconds = expiresAt === null
|
|
37
|
+
? expiresAtMilliseconds
|
|
38
|
+
: (expiresAtMilliseconds ?? expiresAt * 1000);
|
|
39
|
+
this.__expiresAt = this.__expiresAtMilliseconds === null
|
|
40
|
+
? null
|
|
41
|
+
: Math.floor(this.__expiresAtMilliseconds / 1000);
|
|
42
|
+
this.__unadjustedExpiresAtMilliseconds = unadjustedExpiresAtMilliseconds
|
|
43
|
+
?? this.__expiresAtMilliseconds;
|
|
44
|
+
this.__hasPreciseExpiry = hasPreciseExpiry;
|
|
45
|
+
this.__ttlSeconds = Number.isFinite(ttl) ? ttl : null;
|
|
29
46
|
this.__explicitMaxTtl = explicitMaxTtl;
|
|
30
47
|
this.__numUses = numUses;
|
|
31
|
-
this.__isRenewable = isRenewable
|
|
48
|
+
this.__isRenewable = isRenewable
|
|
49
|
+
&& ttl !== 0
|
|
50
|
+
&& this.__expiresAtMilliseconds !== null;
|
|
32
51
|
}
|
|
33
52
|
|
|
34
53
|
static fromResponse(response) {
|
|
35
54
|
const data = response.data;
|
|
55
|
+
const unadjustedExpiresAtMilliseconds = AuthToken.__parseRfc3339Milliseconds(data.expire_time);
|
|
56
|
+
const hasPreciseExpiry = unadjustedExpiresAtMilliseconds !== null;
|
|
57
|
+
const expiresAtMilliseconds = AuthToken.__resolveExpiresAtMilliseconds(data, unadjustedExpiresAtMilliseconds);
|
|
36
58
|
|
|
37
59
|
return new AuthToken(
|
|
38
60
|
data.id,
|
|
39
61
|
data.accessor,
|
|
40
62
|
data.creation_time,
|
|
41
|
-
|
|
63
|
+
null,
|
|
42
64
|
data.explicit_max_ttl,
|
|
43
65
|
data.num_uses,
|
|
44
|
-
data.renewable !== undefined ? data.renewable : false
|
|
66
|
+
data.renewable !== undefined ? data.renewable : false,
|
|
67
|
+
expiresAtMilliseconds,
|
|
68
|
+
data.ttl,
|
|
69
|
+
hasPreciseExpiry,
|
|
70
|
+
unadjustedExpiresAtMilliseconds
|
|
45
71
|
);
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
/**
|
|
49
|
-
* Resolve the UNIX timestamp (
|
|
50
|
-
* as expired
|
|
51
|
-
*
|
|
75
|
+
* Resolve the UNIX timestamp (milliseconds) at which the client should treat the token
|
|
76
|
+
* as expired. A zero TTL is expired when received because the reported TTL is truncated
|
|
77
|
+
* and Vault's wall clock may differ from the client's. For longer TTLs, keep the existing
|
|
78
|
+
* one-minute margin only when more than one minute remains.
|
|
52
79
|
*
|
|
53
80
|
* Prefers Vault's authoritative `expire_time` (RFC3339), which is the documented
|
|
54
81
|
* absolute expiry. Falls back to `(last_renewal_time || creation_time) + ttl` for
|
|
@@ -57,37 +84,41 @@ class AuthToken {
|
|
|
57
84
|
*
|
|
58
85
|
* @private
|
|
59
86
|
* @param {Object} data - the `data` object of a token lookup response
|
|
60
|
-
* @
|
|
87
|
+
* @param {number|null} expireTime - parsed Vault expiry in milliseconds
|
|
88
|
+
* @returns {int|null} client expiry in milliseconds, or null when ttl is 0 without expire_time
|
|
61
89
|
*/
|
|
62
|
-
static
|
|
63
|
-
if (data.ttl === 0) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
|
|
90
|
+
static __resolveExpiresAtMilliseconds(data, expireTime) {
|
|
67
91
|
const networkLatency = 60;
|
|
68
|
-
const margin = data.ttl > networkLatency ? networkLatency : 0;
|
|
69
92
|
|
|
70
|
-
const expireTime = AuthToken.__parseRfc3339Seconds(data.expire_time);
|
|
71
93
|
if (expireTime !== null) {
|
|
72
|
-
|
|
94
|
+
if (data.ttl === 0) {
|
|
95
|
+
return Date.now();
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const margin = data.ttl > networkLatency ? networkLatency : 0;
|
|
99
|
+
return expireTime - margin * 1000;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (data.ttl === 0) {
|
|
103
|
+
return null;
|
|
73
104
|
}
|
|
74
105
|
|
|
75
106
|
// Fallback for responses without a usable `expire_time`.
|
|
76
107
|
const creationTime = parseInt(data.last_renewal_time !== undefined ? data.last_renewal_time : data.creation_time, 10);
|
|
77
108
|
const ttl = data.ttl > networkLatency ? data.ttl - networkLatency : data.ttl;
|
|
78
|
-
return creationTime + parseInt(ttl, 10);
|
|
109
|
+
return (creationTime + parseInt(ttl, 10)) * 1000;
|
|
79
110
|
}
|
|
80
111
|
|
|
81
112
|
/**
|
|
82
113
|
* Parse an RFC3339 timestamp (as returned by Vault, up to nanosecond precision with
|
|
83
|
-
* a timezone offset) into
|
|
114
|
+
* a timezone offset) into UNIX milliseconds. Returns null for missing, empty,
|
|
84
115
|
* unparseable, or non-positive (e.g. Vault's "0001-01-01T00:00:00Z" zero value) input.
|
|
85
116
|
*
|
|
86
117
|
* @private
|
|
87
118
|
* @param {String} value
|
|
88
119
|
* @returns {int|null}
|
|
89
120
|
*/
|
|
90
|
-
static
|
|
121
|
+
static __parseRfc3339Milliseconds(value) {
|
|
91
122
|
if (typeof value !== 'string' || value === '') {
|
|
92
123
|
return null;
|
|
93
124
|
}
|
|
@@ -97,8 +128,7 @@ class AuthToken {
|
|
|
97
128
|
return null;
|
|
98
129
|
}
|
|
99
130
|
|
|
100
|
-
|
|
101
|
-
return seconds > 0 ? seconds : null;
|
|
131
|
+
return ms > 0 ? ms : null;
|
|
102
132
|
}
|
|
103
133
|
|
|
104
134
|
/**
|
|
@@ -120,6 +150,12 @@ class AuthToken {
|
|
|
120
150
|
return false;
|
|
121
151
|
}
|
|
122
152
|
|
|
153
|
+
if (this.__hasPreciseExpiry) {
|
|
154
|
+
return Date.now() >= this.__expiresAtMilliseconds;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Preserve the previous whole-second boundary for tokens that do not carry
|
|
158
|
+
// Vault's authoritative expire_time.
|
|
123
159
|
return Math.floor(Date.now() / 1000) > this.__expiresAt;
|
|
124
160
|
}
|
|
125
161
|
|
|
@@ -127,7 +163,7 @@ class AuthToken {
|
|
|
127
163
|
* @returns {boolean}
|
|
128
164
|
*/
|
|
129
165
|
isRenewable() {
|
|
130
|
-
return this.__isRenewable
|
|
166
|
+
return this.__isRenewable;
|
|
131
167
|
}
|
|
132
168
|
|
|
133
169
|
/**
|
|
@@ -136,6 +172,35 @@ class AuthToken {
|
|
|
136
172
|
getExpiresAt() {
|
|
137
173
|
return this.__expiresAt;
|
|
138
174
|
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* @returns {int|null} precise UNIX timestamp in milliseconds, when available
|
|
178
|
+
*/
|
|
179
|
+
getExpiresAtMilliseconds() {
|
|
180
|
+
return this.__expiresAtMilliseconds;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @returns {int|null} expiry before the safety adjustment, when available
|
|
185
|
+
*/
|
|
186
|
+
getUnadjustedExpiresAtMilliseconds() {
|
|
187
|
+
return this.__unadjustedExpiresAtMilliseconds;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Return Vault's whole-second TTL when this token came from a lookup response.
|
|
192
|
+
* @returns {number|null}
|
|
193
|
+
*/
|
|
194
|
+
getTtlSeconds() {
|
|
195
|
+
return this.__ttlSeconds;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* @returns {boolean}
|
|
200
|
+
*/
|
|
201
|
+
hasPreciseExpiry() {
|
|
202
|
+
return this.__hasPreciseExpiry;
|
|
203
|
+
}
|
|
139
204
|
}
|
|
140
205
|
|
|
141
206
|
module.exports = AuthToken;
|
|
@@ -9,6 +9,8 @@ const AuthToken = require('./AuthToken');
|
|
|
9
9
|
|
|
10
10
|
/** Renew at the halfway point of the token's remaining lifetime. */
|
|
11
11
|
const DEFAULT_RENEWAL_FRACTION = 0.5;
|
|
12
|
+
/** Maximum rounding difference allowed when comparing expiry movement with a renewal interval. */
|
|
13
|
+
const MAX_EXPIRY_JITTER_MS = 1000;
|
|
12
14
|
const errors = require('../errors');
|
|
13
15
|
const safeMessage = require('../safeMessage');
|
|
14
16
|
|
|
@@ -29,6 +31,81 @@ function humanizeMs(ms) {
|
|
|
29
31
|
return `${hours}h ${totalMinutes % 60}m`;
|
|
30
32
|
}
|
|
31
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Read precise expiry when available, preserving the public AuthToken interface fallback.
|
|
36
|
+
* @param {AuthToken} authToken
|
|
37
|
+
* @returns {number|null}
|
|
38
|
+
* @private
|
|
39
|
+
*/
|
|
40
|
+
function tokenExpiryMilliseconds(authToken) {
|
|
41
|
+
if (typeof authToken.getExpiresAtMilliseconds === 'function') {
|
|
42
|
+
const expiry = authToken.getExpiresAtMilliseconds();
|
|
43
|
+
if (Number.isFinite(expiry)) {
|
|
44
|
+
return expiry;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const expiry = authToken.getExpiresAt();
|
|
49
|
+
return typeof expiry === 'number' && Number.isFinite(expiry) ? expiry * 1000 : null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Read Vault's unadjusted absolute expiry when available; duck-typed tokens fall back to their
|
|
54
|
+
* public expiry in seconds.
|
|
55
|
+
* @param {AuthToken} authToken
|
|
56
|
+
* @returns {number|null}
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
function tokenUnadjustedExpiryMilliseconds(authToken) {
|
|
60
|
+
if (typeof authToken.getUnadjustedExpiresAtMilliseconds === 'function') {
|
|
61
|
+
const expiry = authToken.getUnadjustedExpiresAtMilliseconds();
|
|
62
|
+
if (Number.isFinite(expiry)) {
|
|
63
|
+
return expiry;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return tokenExpiryMilliseconds(authToken);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Read Vault's whole-second TTL when the token implementation exposes it.
|
|
72
|
+
* @param {AuthToken} authToken
|
|
73
|
+
* @returns {number|null}
|
|
74
|
+
* @private
|
|
75
|
+
*/
|
|
76
|
+
function tokenTtlSeconds(authToken) {
|
|
77
|
+
if (typeof authToken.getTtlSeconds !== 'function') {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const ttl = authToken.getTtlSeconds();
|
|
81
|
+
return typeof ttl === 'number' && Number.isFinite(ttl) ? ttl : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Explain why background renewal ended without exposing the token ID.
|
|
86
|
+
* @param {VaultBaseAuth} auth
|
|
87
|
+
* @param {AuthToken} authToken
|
|
88
|
+
* @param {string} reason
|
|
89
|
+
* @returns {void}
|
|
90
|
+
* @private
|
|
91
|
+
*/
|
|
92
|
+
function warnRenewalStopped(auth, authToken, reason) {
|
|
93
|
+
const expiry = tokenExpiryMilliseconds(authToken);
|
|
94
|
+
const nextAction = expiry === null
|
|
95
|
+
? 'expiry is unknown; the cached token remains in use until it is replaced'
|
|
96
|
+
: auth._reauthenticationAllowed()
|
|
97
|
+
? 'the next call after expiry will re-authenticate'
|
|
98
|
+
: 'the next call after expiry will fail with AuthTokenExpiredError';
|
|
99
|
+
|
|
100
|
+
auth._log.warn(
|
|
101
|
+
'token renewal stopped (accessor=%s): %s; client-side expiry is %s; %s',
|
|
102
|
+
authToken.getAccessor(),
|
|
103
|
+
reason,
|
|
104
|
+
expiry === null ? 'unknown' : new Date(expiry).toISOString(),
|
|
105
|
+
nextAction
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
32
109
|
class VaultBaseAuth {
|
|
33
110
|
|
|
34
111
|
/**
|
|
@@ -255,12 +332,38 @@ class VaultBaseAuth {
|
|
|
255
332
|
this.__refreshTimeout = null;
|
|
256
333
|
}
|
|
257
334
|
|
|
258
|
-
if (!this.__renewalEnabled
|
|
335
|
+
if (!this.__renewalEnabled) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
if (!authToken.isRenewable() || authToken.isExpired()) {
|
|
339
|
+
warnRenewalStopped(
|
|
340
|
+
this,
|
|
341
|
+
authToken,
|
|
342
|
+
authToken.isExpired() ? 'token already expired' : 'token is not renewable'
|
|
343
|
+
);
|
|
259
344
|
return;
|
|
260
345
|
}
|
|
261
346
|
|
|
262
|
-
const
|
|
263
|
-
|
|
347
|
+
const expiry = tokenExpiryMilliseconds(authToken);
|
|
348
|
+
if (expiry === null) {
|
|
349
|
+
warnRenewalStopped(this, authToken, 'token has no usable expiry');
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const remaining = expiry - Date.now();
|
|
354
|
+
if (remaining <= 0) {
|
|
355
|
+
warnRenewalStopped(this, authToken, 'token already expired');
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const timer = Math.max(
|
|
360
|
+
Math.floor(remaining * this.__renewalFraction),
|
|
361
|
+
250
|
|
362
|
+
);
|
|
363
|
+
if (timer >= remaining) {
|
|
364
|
+
warnRenewalStopped(this, authToken, 'too little lifetime left to renew');
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
264
367
|
|
|
265
368
|
// A renewal that is still in flight when the token is replaced or cancelled must not
|
|
266
369
|
// act on what it finds when it lands: it would overwrite the newer token, or clear
|
|
@@ -268,18 +371,54 @@ class VaultBaseAuth {
|
|
|
268
371
|
// permanently un-renewed. Anything that supersedes this timer bumps the epoch.
|
|
269
372
|
const epoch = ++this.__renewalEpoch;
|
|
270
373
|
const superseded = () => this.__renewalEpoch !== epoch;
|
|
374
|
+
const scheduledAt = Date.now();
|
|
271
375
|
|
|
272
376
|
this.__refreshTimeout = lt.setTimeout(() => {
|
|
273
377
|
if (superseded()) {
|
|
274
378
|
return;
|
|
275
379
|
}
|
|
380
|
+
this.__refreshTimeout = null;
|
|
276
381
|
|
|
277
382
|
this.__renewToken(authToken).then(renewedToken => {
|
|
278
383
|
if (superseded()) {
|
|
279
384
|
return;
|
|
280
385
|
}
|
|
281
386
|
this.__authToken = renewedToken;
|
|
282
|
-
|
|
387
|
+
if (!renewedToken.isRenewable() || renewedToken.isExpired()) {
|
|
388
|
+
this.__setupTokenRefreshTimer(renewedToken);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const previousExpiry = tokenUnadjustedExpiryMilliseconds(authToken);
|
|
393
|
+
const renewedExpiry = tokenUnadjustedExpiryMilliseconds(renewedToken);
|
|
394
|
+
const expiryMovement = previousExpiry === null || renewedExpiry === null
|
|
395
|
+
? null
|
|
396
|
+
: renewedExpiry - previousExpiry;
|
|
397
|
+
const previousTtl = tokenTtlSeconds(authToken);
|
|
398
|
+
const renewedTtl = tokenTtlSeconds(renewedToken);
|
|
399
|
+
const ttlDidNotDecrease = previousTtl === null || renewedTtl === null
|
|
400
|
+
|| renewedTtl >= previousTtl;
|
|
401
|
+
const renewalElapsedMs = Date.now() - scheduledAt;
|
|
402
|
+
// A max-TTL cap leaves the absolute expiry roughly fixed, while a periodic
|
|
403
|
+
// renewal moves it forward by about the time spent waiting. Compare the
|
|
404
|
+
// reported TTL too, so whole-second rounding does not make a short extension
|
|
405
|
+
// look like a capped token.
|
|
406
|
+
const expiryAdvancedWithRenewal = expiryMovement !== null
|
|
407
|
+
&& expiryMovement >= 0
|
|
408
|
+
&& ttlDidNotDecrease
|
|
409
|
+
&& Math.abs(expiryMovement - renewalElapsedMs) <= MAX_EXPIRY_JITTER_MS;
|
|
410
|
+
const expiryMovedMaterially = expiryMovement !== null
|
|
411
|
+
&& Math.abs(expiryMovement) > MAX_EXPIRY_JITTER_MS;
|
|
412
|
+
|
|
413
|
+
if (expiryMovedMaterially || expiryAdvancedWithRenewal) {
|
|
414
|
+
this.__setupTokenRefreshTimer(renewedToken);
|
|
415
|
+
} else {
|
|
416
|
+
warnRenewalStopped(
|
|
417
|
+
this,
|
|
418
|
+
renewedToken,
|
|
419
|
+
'Vault did not extend the token (maximum TTL likely reached)'
|
|
420
|
+
);
|
|
421
|
+
}
|
|
283
422
|
}).catch(err => {
|
|
284
423
|
if (superseded()) {
|
|
285
424
|
return;
|