@webex/webex-core 2.59.2 → 2.59.3-next.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/.eslintrc.js +6 -6
- package/README.md +79 -79
- package/babel.config.js +3 -3
- package/dist/config.js +24 -24
- package/dist/config.js.map +1 -1
- package/dist/credentials-config.js +56 -56
- package/dist/credentials-config.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/interceptors/auth.js +28 -28
- package/dist/interceptors/auth.js.map +1 -1
- package/dist/interceptors/default-options.js +24 -24
- package/dist/interceptors/default-options.js.map +1 -1
- package/dist/interceptors/embargo.js +9 -9
- package/dist/interceptors/embargo.js.map +1 -1
- package/dist/interceptors/network-timing.js +19 -19
- package/dist/interceptors/network-timing.js.map +1 -1
- package/dist/interceptors/payload-transformer.js +19 -19
- package/dist/interceptors/payload-transformer.js.map +1 -1
- package/dist/interceptors/rate-limit.js +40 -40
- package/dist/interceptors/rate-limit.js.map +1 -1
- package/dist/interceptors/redirect.js +13 -13
- package/dist/interceptors/redirect.js.map +1 -1
- package/dist/interceptors/request-event.js +23 -23
- package/dist/interceptors/request-event.js.map +1 -1
- package/dist/interceptors/request-logger.js +13 -13
- package/dist/interceptors/request-logger.js.map +1 -1
- package/dist/interceptors/request-timing.js +23 -23
- package/dist/interceptors/request-timing.js.map +1 -1
- package/dist/interceptors/response-logger.js +19 -19
- package/dist/interceptors/response-logger.js.map +1 -1
- package/dist/interceptors/user-agent.js +29 -29
- package/dist/interceptors/user-agent.js.map +1 -1
- package/dist/interceptors/webex-tracking-id.js +15 -15
- package/dist/interceptors/webex-tracking-id.js.map +1 -1
- package/dist/interceptors/webex-user-agent.js +13 -13
- package/dist/interceptors/webex-user-agent.js.map +1 -1
- package/dist/lib/batcher.js +83 -83
- package/dist/lib/batcher.js.map +1 -1
- package/dist/lib/credentials/credentials.js +103 -103
- package/dist/lib/credentials/credentials.js.map +1 -1
- package/dist/lib/credentials/grant-errors.js +17 -17
- package/dist/lib/credentials/grant-errors.js.map +1 -1
- package/dist/lib/credentials/index.js +2 -2
- package/dist/lib/credentials/index.js.map +1 -1
- package/dist/lib/credentials/scope.js +11 -11
- package/dist/lib/credentials/scope.js.map +1 -1
- package/dist/lib/credentials/token-collection.js +2 -2
- package/dist/lib/credentials/token-collection.js.map +1 -1
- package/dist/lib/credentials/token.js +145 -145
- package/dist/lib/credentials/token.js.map +1 -1
- package/dist/lib/page.js +49 -49
- package/dist/lib/page.js.map +1 -1
- package/dist/lib/services/constants.js.map +1 -1
- package/dist/lib/services/index.js +2 -2
- package/dist/lib/services/index.js.map +1 -1
- package/dist/lib/services/interceptors/server-error.js +9 -9
- package/dist/lib/services/interceptors/server-error.js.map +1 -1
- package/dist/lib/services/interceptors/service.js +24 -24
- package/dist/lib/services/interceptors/service.js.map +1 -1
- package/dist/lib/services/metrics.js.map +1 -1
- package/dist/lib/services/service-catalog.js +104 -104
- package/dist/lib/services/service-catalog.js.map +1 -1
- package/dist/lib/services/service-fed-ramp.js.map +1 -1
- package/dist/lib/services/service-host.js +134 -134
- package/dist/lib/services/service-host.js.map +1 -1
- package/dist/lib/services/service-registry.js +175 -175
- package/dist/lib/services/service-registry.js.map +1 -1
- package/dist/lib/services/service-state.js +38 -38
- package/dist/lib/services/service-state.js.map +1 -1
- package/dist/lib/services/service-url.js +31 -31
- package/dist/lib/services/service-url.js.map +1 -1
- package/dist/lib/services/services.js +245 -245
- package/dist/lib/services/services.js.map +1 -1
- package/dist/lib/stateless-webex-plugin.js +28 -28
- package/dist/lib/stateless-webex-plugin.js.map +1 -1
- package/dist/lib/storage/decorators.js +27 -27
- package/dist/lib/storage/decorators.js.map +1 -1
- package/dist/lib/storage/errors.js +4 -4
- package/dist/lib/storage/errors.js.map +1 -1
- package/dist/lib/storage/index.js.map +1 -1
- package/dist/lib/storage/make-webex-plugin-store.js +44 -44
- package/dist/lib/storage/make-webex-plugin-store.js.map +1 -1
- package/dist/lib/storage/make-webex-store.js +40 -40
- package/dist/lib/storage/make-webex-store.js.map +1 -1
- package/dist/lib/storage/memory-store-adapter.js +9 -9
- package/dist/lib/storage/memory-store-adapter.js.map +1 -1
- package/dist/lib/webex-core-plugin-mixin.js +13 -13
- package/dist/lib/webex-core-plugin-mixin.js.map +1 -1
- package/dist/lib/webex-http-error.js +9 -9
- package/dist/lib/webex-http-error.js.map +1 -1
- package/dist/lib/webex-internal-core-plugin-mixin.js +13 -13
- package/dist/lib/webex-internal-core-plugin-mixin.js.map +1 -1
- package/dist/lib/webex-plugin.js +36 -36
- package/dist/lib/webex-plugin.js.map +1 -1
- package/dist/plugins/logger.js +9 -9
- package/dist/plugins/logger.js.map +1 -1
- package/dist/webex-core.js +104 -104
- package/dist/webex-core.js.map +1 -1
- package/dist/webex-internal-core.js +12 -12
- package/dist/webex-internal-core.js.map +1 -1
- package/jest.config.js +3 -3
- package/package.json +20 -19
- package/process +1 -1
- package/src/config.js +90 -90
- package/src/credentials-config.js +212 -212
- package/src/index.js +62 -62
- package/src/interceptors/auth.js +186 -186
- package/src/interceptors/default-options.js +55 -55
- package/src/interceptors/embargo.js +43 -43
- package/src/interceptors/network-timing.js +54 -54
- package/src/interceptors/payload-transformer.js +55 -55
- package/src/interceptors/rate-limit.js +169 -169
- package/src/interceptors/redirect.js +106 -106
- package/src/interceptors/request-event.js +93 -93
- package/src/interceptors/request-logger.js +78 -78
- package/src/interceptors/request-timing.js +65 -65
- package/src/interceptors/response-logger.js +98 -98
- package/src/interceptors/user-agent.js +77 -77
- package/src/interceptors/webex-tracking-id.js +73 -73
- package/src/interceptors/webex-user-agent.js +79 -79
- package/src/lib/batcher.js +307 -307
- package/src/lib/credentials/credentials.js +552 -552
- package/src/lib/credentials/grant-errors.js +92 -92
- package/src/lib/credentials/index.js +16 -16
- package/src/lib/credentials/scope.js +34 -34
- package/src/lib/credentials/token-collection.js +17 -17
- package/src/lib/credentials/token.js +559 -559
- package/src/lib/page.js +159 -159
- package/src/lib/services/constants.js +9 -9
- package/src/lib/services/index.js +26 -26
- package/src/lib/services/interceptors/server-error.js +48 -48
- package/src/lib/services/interceptors/service.js +101 -101
- package/src/lib/services/metrics.js +4 -4
- package/src/lib/services/service-catalog.js +435 -435
- package/src/lib/services/service-fed-ramp.js +4 -4
- package/src/lib/services/service-host.js +267 -267
- package/src/lib/services/service-registry.js +465 -465
- package/src/lib/services/service-state.js +78 -78
- package/src/lib/services/service-url.js +124 -124
- package/src/lib/services/services.js +1018 -1018
- package/src/lib/stateless-webex-plugin.js +98 -98
- package/src/lib/storage/decorators.js +220 -220
- package/src/lib/storage/errors.js +15 -15
- package/src/lib/storage/index.js +10 -10
- package/src/lib/storage/make-webex-plugin-store.js +211 -211
- package/src/lib/storage/make-webex-store.js +140 -140
- package/src/lib/storage/memory-store-adapter.js +79 -79
- package/src/lib/webex-core-plugin-mixin.js +114 -114
- package/src/lib/webex-http-error.js +61 -61
- package/src/lib/webex-internal-core-plugin-mixin.js +107 -107
- package/src/lib/webex-plugin.js +222 -222
- package/src/plugins/logger.js +60 -60
- package/src/webex-core.js +745 -745
- package/src/webex-internal-core.js +46 -46
- package/test/integration/spec/credentials/credentials.js +139 -139
- package/test/integration/spec/credentials/token.js +102 -102
- package/test/integration/spec/services/service-catalog.js +838 -838
- package/test/integration/spec/services/services.js +1221 -1221
- package/test/integration/spec/webex-core.js +178 -178
- package/test/unit/spec/_setup.js +44 -44
- package/test/unit/spec/credentials/credentials.js +1017 -1017
- package/test/unit/spec/credentials/token.js +441 -441
- package/test/unit/spec/interceptors/auth.js +521 -521
- package/test/unit/spec/interceptors/default-options.js +84 -84
- package/test/unit/spec/interceptors/embargo.js +144 -144
- package/test/unit/spec/interceptors/network-timing.js +49 -49
- package/test/unit/spec/interceptors/payload-transformer.js +155 -155
- package/test/unit/spec/interceptors/rate-limit.js +302 -302
- package/test/unit/spec/interceptors/redirect.js +102 -102
- package/test/unit/spec/interceptors/request-timing.js +92 -92
- package/test/unit/spec/interceptors/user-agent.js +76 -76
- package/test/unit/spec/interceptors/webex-tracking-id.js +76 -76
- package/test/unit/spec/interceptors/webex-user-agent.js +159 -159
- package/test/unit/spec/lib/batcher.js +330 -330
- package/test/unit/spec/lib/page.js +148 -148
- package/test/unit/spec/lib/webex-plugin.js +48 -48
- package/test/unit/spec/services/interceptors/server-error.js +204 -204
- package/test/unit/spec/services/interceptors/service.js +188 -188
- package/test/unit/spec/services/service-catalog.js +194 -194
- package/test/unit/spec/services/service-host.js +260 -260
- package/test/unit/spec/services/service-registry.js +747 -747
- package/test/unit/spec/services/service-state.js +60 -60
- package/test/unit/spec/services/service-url.js +258 -258
- package/test/unit/spec/services/services.js +348 -348
- package/test/unit/spec/storage/persist.js +50 -50
- package/test/unit/spec/storage/storage-adapter.js +12 -12
- package/test/unit/spec/storage/wait-for-value.js +81 -81
- package/test/unit/spec/webex-core.js +253 -253
- package/test/unit/spec/webex-internal-core.js +91 -91
|
@@ -1,559 +1,559 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {pick} from 'lodash';
|
|
6
|
-
import {inBrowser, oneFlight} from '@webex/common';
|
|
7
|
-
import {safeSetTimeout} from '@webex/common-timers';
|
|
8
|
-
|
|
9
|
-
import WebexHttpError from '../webex-http-error';
|
|
10
|
-
import WebexPlugin from '../webex-plugin';
|
|
11
|
-
|
|
12
|
-
import {sortScope} from './scope';
|
|
13
|
-
import grantErrors, {OAuthError} from './grant-errors';
|
|
14
|
-
|
|
15
|
-
/* eslint-disable camelcase */
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* Parse response from CI and converts to structured error when appropriate
|
|
19
|
-
* @param {WebexHttpError} res
|
|
20
|
-
* @private
|
|
21
|
-
* @returns {GrantError}
|
|
22
|
-
*/
|
|
23
|
-
function processGrantError(res) {
|
|
24
|
-
if (res.statusCode !== 400) {
|
|
25
|
-
return Promise.reject(res);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const ErrorConstructor = grantErrors.select(res.body.error);
|
|
29
|
-
|
|
30
|
-
if (ErrorConstructor === OAuthError && res instanceof WebexHttpError) {
|
|
31
|
-
return Promise.reject(res);
|
|
32
|
-
}
|
|
33
|
-
if (!ErrorConstructor) {
|
|
34
|
-
return Promise.reject(res);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return Promise.reject(new ErrorConstructor(res._res || res));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @class
|
|
42
|
-
*/
|
|
43
|
-
const Token = WebexPlugin.extend({
|
|
44
|
-
derived: {
|
|
45
|
-
/**
|
|
46
|
-
* Indicates if this token can be used in an auth header. `true` iff
|
|
47
|
-
* {@link Token#access_token} is defined and {@link Token#isExpired} is
|
|
48
|
-
* false.
|
|
49
|
-
* @instance
|
|
50
|
-
* @memberof Token
|
|
51
|
-
* @readonly
|
|
52
|
-
* @type {boolean}
|
|
53
|
-
*/
|
|
54
|
-
canAuthorize: {
|
|
55
|
-
deps: ['access_token', 'isExpired'],
|
|
56
|
-
fn() {
|
|
57
|
-
return !!this.access_token && !this.isExpired;
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Indicates that this token can be downscoped. `true` iff
|
|
63
|
-
* {@link config.credentials.client_id} is defined and if
|
|
64
|
-
* {@link Token#canAuthorize} is true
|
|
65
|
-
*
|
|
66
|
-
* Note: since {@link config} is not evented, we can't listen for changes to
|
|
67
|
-
* {@link config.credentials.client_id}. As such,
|
|
68
|
-
* {@link config.credentials.client_id} must always be set before
|
|
69
|
-
* instantiating a {@link Token}
|
|
70
|
-
* @instance
|
|
71
|
-
* @memberof Token
|
|
72
|
-
* @readonly
|
|
73
|
-
* @type {boolean}
|
|
74
|
-
*/
|
|
75
|
-
canDownscope: {
|
|
76
|
-
deps: ['canAuthorize'],
|
|
77
|
-
fn() {
|
|
78
|
-
return this.canAuthorize && !!this.config.client_id;
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Indicates if this token can be refreshed. `true` iff
|
|
84
|
-
* {@link Token@refresh_token} is defined and
|
|
85
|
-
* {@link config.credentials.refreshCallback()} is defined
|
|
86
|
-
*
|
|
87
|
-
* Note: since {@link config} is not evented, we can't listen for changes to
|
|
88
|
-
* {@link config.credentials.refreshCallback()}. As such,
|
|
89
|
-
* {@link config.credentials.refreshCallback()} must always be set before
|
|
90
|
-
* instantiating a {@link Token}
|
|
91
|
-
* @instance
|
|
92
|
-
* @memberof Token
|
|
93
|
-
* @readonly
|
|
94
|
-
* @type {boolean}
|
|
95
|
-
*/
|
|
96
|
-
canRefresh: {
|
|
97
|
-
deps: ['refresh_token'],
|
|
98
|
-
fn() {
|
|
99
|
-
if (inBrowser) {
|
|
100
|
-
return !!this.refresh_token && !!this.config.refreshCallback;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return !!this.refresh_token && !!this.config.client_secret;
|
|
104
|
-
},
|
|
105
|
-
},
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Indicates if this `Token` is expired. `true` iff {@link Token#expires} is
|
|
109
|
-
* defined and is less than {@link Date.now()}.
|
|
110
|
-
* @instance
|
|
111
|
-
* @memberof Token
|
|
112
|
-
* @readonly
|
|
113
|
-
* @type {boolean}
|
|
114
|
-
*/
|
|
115
|
-
isExpired: {
|
|
116
|
-
deps: ['expires', '_isExpired'],
|
|
117
|
-
fn() {
|
|
118
|
-
// in order to avoid setting `cache:false`, we'll use a private property
|
|
119
|
-
// and a timer rather than comparing to `Date.now()`;
|
|
120
|
-
return !!this.expires && this._isExpired;
|
|
121
|
-
},
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Cache for toString()
|
|
126
|
-
* @instance
|
|
127
|
-
* @memberof Token
|
|
128
|
-
* @private
|
|
129
|
-
* @readonly
|
|
130
|
-
* @type {string}
|
|
131
|
-
*/
|
|
132
|
-
_string: {
|
|
133
|
-
deps: ['access_token', 'token_type'],
|
|
134
|
-
fn() {
|
|
135
|
-
if (!this.access_token || !this.token_type) {
|
|
136
|
-
return '';
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
return `${this.token_type} ${this.access_token}`;
|
|
140
|
-
},
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
|
|
144
|
-
namespace: 'Credentials',
|
|
145
|
-
|
|
146
|
-
props: {
|
|
147
|
-
/**
|
|
148
|
-
* Used for indexing in the credentials userTokens collection
|
|
149
|
-
* @instance
|
|
150
|
-
* @memberof Token
|
|
151
|
-
* @private
|
|
152
|
-
* @type {string}
|
|
153
|
-
*/
|
|
154
|
-
scope: 'string',
|
|
155
|
-
/**
|
|
156
|
-
* @instance
|
|
157
|
-
* @memberof Token
|
|
158
|
-
* @type {string}
|
|
159
|
-
*/
|
|
160
|
-
access_token: 'string',
|
|
161
|
-
/**
|
|
162
|
-
* @instance
|
|
163
|
-
* @memberof Token
|
|
164
|
-
* @type {number}
|
|
165
|
-
*/
|
|
166
|
-
expires: 'number',
|
|
167
|
-
/**
|
|
168
|
-
* @instance
|
|
169
|
-
* @memberof Token
|
|
170
|
-
* @type {number}
|
|
171
|
-
*/
|
|
172
|
-
expires_in: 'number',
|
|
173
|
-
/**
|
|
174
|
-
* @instance
|
|
175
|
-
* @memberof Token
|
|
176
|
-
* @type {string}
|
|
177
|
-
*/
|
|
178
|
-
refresh_token: 'string',
|
|
179
|
-
/**
|
|
180
|
-
* @instance
|
|
181
|
-
* @memberof Token
|
|
182
|
-
* @type {number}
|
|
183
|
-
*/
|
|
184
|
-
refresh_token_expires: 'number',
|
|
185
|
-
/**
|
|
186
|
-
* @instance
|
|
187
|
-
* @memberof Token
|
|
188
|
-
* @type {number}
|
|
189
|
-
*/
|
|
190
|
-
refresh_token_expires_in: 'number',
|
|
191
|
-
/**
|
|
192
|
-
* @default "Bearer"
|
|
193
|
-
* @instance
|
|
194
|
-
* @memberof Token
|
|
195
|
-
* @type {string}
|
|
196
|
-
*/
|
|
197
|
-
token_type: {
|
|
198
|
-
default: 'Bearer',
|
|
199
|
-
type: 'string',
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
|
|
203
|
-
session: {
|
|
204
|
-
/**
|
|
205
|
-
* Used by {@link Token#isExpired} to avoid doing a Date comparison.
|
|
206
|
-
* @instance
|
|
207
|
-
* @memberof Token
|
|
208
|
-
* @private
|
|
209
|
-
* @type {boolean}
|
|
210
|
-
*/
|
|
211
|
-
_isExpired: {
|
|
212
|
-
default: false,
|
|
213
|
-
type: 'boolean',
|
|
214
|
-
},
|
|
215
|
-
/**
|
|
216
|
-
* Handle to the previous token that we'll revoke when we refresh this
|
|
217
|
-
* token. The idea is to keep allow two valid tokens when a refresh occurs;
|
|
218
|
-
* we don't want revoke a token that's in the middle of being used, so when
|
|
219
|
-
* we do a token refresh, we won't revoke the token being refreshed, but
|
|
220
|
-
* we'll revoke the previous one.
|
|
221
|
-
* @instance
|
|
222
|
-
* @memberof Token
|
|
223
|
-
* @private
|
|
224
|
-
* @type {Object}
|
|
225
|
-
*/
|
|
226
|
-
previousToken: {
|
|
227
|
-
type: 'state',
|
|
228
|
-
},
|
|
229
|
-
},
|
|
230
|
-
|
|
231
|
-
@oneFlight({
|
|
232
|
-
keyFactory(scope) {
|
|
233
|
-
return scope;
|
|
234
|
-
},
|
|
235
|
-
})
|
|
236
|
-
/**
|
|
237
|
-
* Uses this token to request a new Token with a subset of this Token's scopes
|
|
238
|
-
* @instance
|
|
239
|
-
* @memberof Token
|
|
240
|
-
* @param {string} scope
|
|
241
|
-
* @returns {Promise<Token>}
|
|
242
|
-
*/
|
|
243
|
-
downscope(scope) {
|
|
244
|
-
this.logger.info(`token: downscoping token to ${scope}`);
|
|
245
|
-
|
|
246
|
-
if (this.isExpired) {
|
|
247
|
-
this.logger.info('token: request received to downscope expired access_token');
|
|
248
|
-
|
|
249
|
-
return Promise.reject(new Error('cannot downscope expired access token'));
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!this.canDownscope) {
|
|
253
|
-
if (this.config.client_id) {
|
|
254
|
-
this.logger.info('token: request received to downscope invalid access_token');
|
|
255
|
-
} else {
|
|
256
|
-
this.logger.trace('token: cannot downscope without client_id');
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
return Promise.reject(new Error('cannot downscope access token'));
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Since we're going to use scope as the index in our token collection, it's
|
|
263
|
-
// important scopes are always deterministically specified.
|
|
264
|
-
if (scope) {
|
|
265
|
-
scope = sortScope(scope);
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Ideally, we could depend on the service to communicate this error, but
|
|
269
|
-
// all we get is "invalid scope", which, to the lay person, implies
|
|
270
|
-
// something wrong with *one* of the scopes, not the whole thing.
|
|
271
|
-
if (scope === sortScope(this.config.scope)) {
|
|
272
|
-
return Promise.reject(new Error('token: scope reduction requires a reduced scope'));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return this.webex
|
|
276
|
-
.request({
|
|
277
|
-
method: 'POST',
|
|
278
|
-
uri: this.config.tokenUrl,
|
|
279
|
-
addAuthHeader: false,
|
|
280
|
-
form: {
|
|
281
|
-
grant_type: 'urn:cisco:oauth:grant-type:scope-reduction',
|
|
282
|
-
token: this.access_token,
|
|
283
|
-
scope,
|
|
284
|
-
client_id: this.config.client_id,
|
|
285
|
-
self_contained_token: true,
|
|
286
|
-
},
|
|
287
|
-
})
|
|
288
|
-
.then((res) => {
|
|
289
|
-
this.logger.info(`token: downscoped token to ${scope}`);
|
|
290
|
-
|
|
291
|
-
return new Token(Object.assign(res.body, {scope}), {parent: this.parent});
|
|
292
|
-
});
|
|
293
|
-
},
|
|
294
|
-
|
|
295
|
-
/**
|
|
296
|
-
* Initializer
|
|
297
|
-
* @instance
|
|
298
|
-
* @memberof Token
|
|
299
|
-
* @param {Object} [attrs={}]
|
|
300
|
-
* @param {Object} [options={}]
|
|
301
|
-
* @see {@link WebexPlugin#initialize()}
|
|
302
|
-
* @returns {Token}
|
|
303
|
-
*/
|
|
304
|
-
initialize(attrs = {}, options = {}) {
|
|
305
|
-
Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
|
|
306
|
-
|
|
307
|
-
if (typeof attrs === 'string') {
|
|
308
|
-
this.access_token = attrs;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
if (!this.access_token) {
|
|
312
|
-
throw new Error('`access_token` is required');
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// We don't want the derived property `isExpired` to need {cache:false}, so
|
|
316
|
-
// we'll set up a timer the runs when this token should expire.
|
|
317
|
-
if (this.expires) {
|
|
318
|
-
if (this.expires < Date.now()) {
|
|
319
|
-
this._isExpired = true;
|
|
320
|
-
} else {
|
|
321
|
-
safeSetTimeout(() => {
|
|
322
|
-
this._isExpired = true;
|
|
323
|
-
}, this.expires - Date.now());
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
},
|
|
327
|
-
|
|
328
|
-
@oneFlight
|
|
329
|
-
/**
|
|
330
|
-
* Refreshes this Token. Relies on
|
|
331
|
-
* {@link config.credentials.refreshCallback()}
|
|
332
|
-
* @instance
|
|
333
|
-
* @memberof Token
|
|
334
|
-
* @returns {Promise<Token>}
|
|
335
|
-
*/
|
|
336
|
-
refresh() {
|
|
337
|
-
if (!this.canRefresh) {
|
|
338
|
-
throw new Error('Not enough information available to refresh this access token');
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
let promise;
|
|
342
|
-
|
|
343
|
-
if (inBrowser) {
|
|
344
|
-
if (!this.config.refreshCallback) {
|
|
345
|
-
throw new Error('Cannot refresh access token without refreshCallback');
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
promise = Promise.resolve(this.config.refreshCallback(this.webex, this));
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
return (
|
|
352
|
-
promise ||
|
|
353
|
-
this.webex
|
|
354
|
-
.request({
|
|
355
|
-
method: 'POST',
|
|
356
|
-
uri: this.config.tokenUrl,
|
|
357
|
-
form: {
|
|
358
|
-
grant_type: 'refresh_token',
|
|
359
|
-
redirect_uri: this.config.redirect_uri,
|
|
360
|
-
refresh_token: this.refresh_token,
|
|
361
|
-
},
|
|
362
|
-
auth: {
|
|
363
|
-
user: this.config.client_id,
|
|
364
|
-
pass: this.config.client_secret,
|
|
365
|
-
sendImmediately: true,
|
|
366
|
-
},
|
|
367
|
-
shouldRefreshAccessToken: false,
|
|
368
|
-
})
|
|
369
|
-
.then((res) => res.body)
|
|
370
|
-
)
|
|
371
|
-
.then((obj) => {
|
|
372
|
-
if (!obj) {
|
|
373
|
-
throw new Error('token: refreshCallback() did not produce an object');
|
|
374
|
-
}
|
|
375
|
-
// If the authentication server did not send back a refresh token, copy
|
|
376
|
-
// the current refresh token and related values to the response (note:
|
|
377
|
-
// at time of implementation, CI never sends a new refresh token)
|
|
378
|
-
if (!obj.refresh_token) {
|
|
379
|
-
Object.assign(
|
|
380
|
-
obj,
|
|
381
|
-
pick(this, 'refresh_token', 'refresh_token_expires', 'refresh_token_expires_in')
|
|
382
|
-
);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
// If the new token is the same as the previous token, then we may have
|
|
386
|
-
// found a bug in CI; log the details and reject the Promise
|
|
387
|
-
if (this.access_token === obj.access_token) {
|
|
388
|
-
this.logger.error('token: new token matches current token');
|
|
389
|
-
// log the tokens if it is not production
|
|
390
|
-
if (process.env.NODE_ENV !== 'production') {
|
|
391
|
-
this.logger.error('token: current token:', this.access_token);
|
|
392
|
-
this.logger.error('token: new token:', obj.access_token);
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
return Promise.reject(new Error('new token matches current token'));
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
if (this.previousToken) {
|
|
399
|
-
this.previousToken.revoke();
|
|
400
|
-
this.unset('previousToken');
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
obj.previousToken = this;
|
|
404
|
-
obj.scope = this.scope;
|
|
405
|
-
|
|
406
|
-
return new Token(obj, {parent: this.parent});
|
|
407
|
-
})
|
|
408
|
-
.catch(processGrantError);
|
|
409
|
-
},
|
|
410
|
-
|
|
411
|
-
@oneFlight
|
|
412
|
-
/**
|
|
413
|
-
* Revokes this token and unsets its local properties
|
|
414
|
-
* @instance
|
|
415
|
-
* @memberof Token
|
|
416
|
-
* @returns {Promise}
|
|
417
|
-
*/
|
|
418
|
-
revoke() {
|
|
419
|
-
if (this.isExpired) {
|
|
420
|
-
this.logger.info('token: already expired, not making making revocation request');
|
|
421
|
-
|
|
422
|
-
return Promise.resolve();
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
if (!this.canAuthorize) {
|
|
426
|
-
this.logger.info('token: no longer valid, not making revocation request');
|
|
427
|
-
|
|
428
|
-
return Promise.resolve();
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
// FIXME we need to use the user token revocation endpoint to revoke a token
|
|
432
|
-
// without a client_secret, but it doesn't current support using a token to
|
|
433
|
-
// revoke itself
|
|
434
|
-
// Note: I'm not making a canRevoke property because there should be changes
|
|
435
|
-
// coming to the user token revocation endpoint that allow us to do this
|
|
436
|
-
// correctly.
|
|
437
|
-
if (!this.config.client_secret) {
|
|
438
|
-
this.logger.info('token: no client secret available, not making revocation request');
|
|
439
|
-
|
|
440
|
-
return Promise.resolve();
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
this.logger.info('token: revoking access token');
|
|
444
|
-
|
|
445
|
-
return this.webex
|
|
446
|
-
.request({
|
|
447
|
-
method: 'POST',
|
|
448
|
-
uri: this.config.revokeUrl,
|
|
449
|
-
form: {
|
|
450
|
-
token: this.access_token,
|
|
451
|
-
token_type_hint: 'access_token',
|
|
452
|
-
},
|
|
453
|
-
auth: {
|
|
454
|
-
user: this.config.client_id,
|
|
455
|
-
pass: this.config.client_secret,
|
|
456
|
-
sendImmediately: true,
|
|
457
|
-
},
|
|
458
|
-
shouldRefreshAccessToken: false,
|
|
459
|
-
})
|
|
460
|
-
.then(() => {
|
|
461
|
-
this.unset(['access_token', 'expires', 'expires_in', 'token_type']);
|
|
462
|
-
this.logger.info('token: access token revoked');
|
|
463
|
-
})
|
|
464
|
-
.catch(processGrantError);
|
|
465
|
-
},
|
|
466
|
-
|
|
467
|
-
set(...args) {
|
|
468
|
-
// eslint-disable-next-line prefer-const
|
|
469
|
-
let [attrs, options] = this._filterSetParameters(...args);
|
|
470
|
-
|
|
471
|
-
if (!attrs.token_type && attrs.access_token && attrs.access_token.includes(' ')) {
|
|
472
|
-
const [token_type, access_token] = attrs.access_token.split(' ');
|
|
473
|
-
|
|
474
|
-
attrs = {...attrs, access_token, token_type};
|
|
475
|
-
}
|
|
476
|
-
const now = Date.now();
|
|
477
|
-
|
|
478
|
-
if (!attrs.expires && attrs.expires_in) {
|
|
479
|
-
attrs.expires = now + attrs.expires_in * 1000;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
if (!attrs.refresh_token_expires && attrs.refresh_token_expires_in) {
|
|
483
|
-
attrs.refresh_token_expires = now + attrs.refresh_token_expires_in * 1000;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
if (attrs.scope) {
|
|
487
|
-
attrs.scope = sortScope(attrs.scope);
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
return Reflect.apply(WebexPlugin.prototype.set, this, [attrs, options]);
|
|
491
|
-
},
|
|
492
|
-
|
|
493
|
-
/**
|
|
494
|
-
* Renders the token object as an HTTP Header Value
|
|
495
|
-
* @instance
|
|
496
|
-
* @memberof Token
|
|
497
|
-
* @returns {string}
|
|
498
|
-
* @see {@link Object#toString()}
|
|
499
|
-
*/
|
|
500
|
-
toString() {
|
|
501
|
-
if (!this._string) {
|
|
502
|
-
throw new Error('cannot stringify Token');
|
|
503
|
-
}
|
|
504
|
-
|
|
505
|
-
return this._string;
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Uses a non-producation api to return information about this token. This
|
|
510
|
-
* method is primarily for tests and will throw if NODE_ENV === production
|
|
511
|
-
* @instance
|
|
512
|
-
* @memberof Token
|
|
513
|
-
* @private
|
|
514
|
-
* @returns {Promise}
|
|
515
|
-
*/
|
|
516
|
-
validate() {
|
|
517
|
-
if (process.env.NODE_ENV === 'production') {
|
|
518
|
-
throw new Error('Token#validate() must not be used in production');
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
return this.webex
|
|
522
|
-
.request({
|
|
523
|
-
method: 'POST',
|
|
524
|
-
service: 'conversation',
|
|
525
|
-
resource: 'users/validateAuthToken',
|
|
526
|
-
body: {
|
|
527
|
-
token: this.access_token,
|
|
528
|
-
},
|
|
529
|
-
})
|
|
530
|
-
.catch((reason) => {
|
|
531
|
-
if ('statusCode' in reason) {
|
|
532
|
-
return Promise.reject(reason);
|
|
533
|
-
}
|
|
534
|
-
this.logger.info("REMINDER: If you're investigating a network error here, it's normal");
|
|
535
|
-
|
|
536
|
-
// If we got an error that isn't a WebexHttpError, assume the problem is
|
|
537
|
-
// that we don't have the wdm plugin loaded and service/resource isn't
|
|
538
|
-
// a valid means of identifying a request.
|
|
539
|
-
const convApi =
|
|
540
|
-
process.env.CONVERSATION_SERVICE ||
|
|
541
|
-
process.env.CONVERSATION_SERVICE_URL ||
|
|
542
|
-
'https://conv-a.wbx2.com/conversation/api/v1';
|
|
543
|
-
|
|
544
|
-
return this.webex.request({
|
|
545
|
-
method: 'POST',
|
|
546
|
-
uri: `${convApi}/users/validateAuthToken`,
|
|
547
|
-
body: {
|
|
548
|
-
token: this.access_token,
|
|
549
|
-
},
|
|
550
|
-
headers: {
|
|
551
|
-
authorization: `Bearer ${this.access_token}`,
|
|
552
|
-
},
|
|
553
|
-
});
|
|
554
|
-
})
|
|
555
|
-
.then((res) => res.body);
|
|
556
|
-
},
|
|
557
|
-
});
|
|
558
|
-
|
|
559
|
-
export default Token;
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {pick} from 'lodash';
|
|
6
|
+
import {inBrowser, oneFlight} from '@webex/common';
|
|
7
|
+
import {safeSetTimeout} from '@webex/common-timers';
|
|
8
|
+
|
|
9
|
+
import WebexHttpError from '../webex-http-error';
|
|
10
|
+
import WebexPlugin from '../webex-plugin';
|
|
11
|
+
|
|
12
|
+
import {sortScope} from './scope';
|
|
13
|
+
import grantErrors, {OAuthError} from './grant-errors';
|
|
14
|
+
|
|
15
|
+
/* eslint-disable camelcase */
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Parse response from CI and converts to structured error when appropriate
|
|
19
|
+
* @param {WebexHttpError} res
|
|
20
|
+
* @private
|
|
21
|
+
* @returns {GrantError}
|
|
22
|
+
*/
|
|
23
|
+
function processGrantError(res) {
|
|
24
|
+
if (res.statusCode !== 400) {
|
|
25
|
+
return Promise.reject(res);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const ErrorConstructor = grantErrors.select(res.body.error);
|
|
29
|
+
|
|
30
|
+
if (ErrorConstructor === OAuthError && res instanceof WebexHttpError) {
|
|
31
|
+
return Promise.reject(res);
|
|
32
|
+
}
|
|
33
|
+
if (!ErrorConstructor) {
|
|
34
|
+
return Promise.reject(res);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Promise.reject(new ErrorConstructor(res._res || res));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* @class
|
|
42
|
+
*/
|
|
43
|
+
const Token = WebexPlugin.extend({
|
|
44
|
+
derived: {
|
|
45
|
+
/**
|
|
46
|
+
* Indicates if this token can be used in an auth header. `true` iff
|
|
47
|
+
* {@link Token#access_token} is defined and {@link Token#isExpired} is
|
|
48
|
+
* false.
|
|
49
|
+
* @instance
|
|
50
|
+
* @memberof Token
|
|
51
|
+
* @readonly
|
|
52
|
+
* @type {boolean}
|
|
53
|
+
*/
|
|
54
|
+
canAuthorize: {
|
|
55
|
+
deps: ['access_token', 'isExpired'],
|
|
56
|
+
fn() {
|
|
57
|
+
return !!this.access_token && !this.isExpired;
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Indicates that this token can be downscoped. `true` iff
|
|
63
|
+
* {@link config.credentials.client_id} is defined and if
|
|
64
|
+
* {@link Token#canAuthorize} is true
|
|
65
|
+
*
|
|
66
|
+
* Note: since {@link config} is not evented, we can't listen for changes to
|
|
67
|
+
* {@link config.credentials.client_id}. As such,
|
|
68
|
+
* {@link config.credentials.client_id} must always be set before
|
|
69
|
+
* instantiating a {@link Token}
|
|
70
|
+
* @instance
|
|
71
|
+
* @memberof Token
|
|
72
|
+
* @readonly
|
|
73
|
+
* @type {boolean}
|
|
74
|
+
*/
|
|
75
|
+
canDownscope: {
|
|
76
|
+
deps: ['canAuthorize'],
|
|
77
|
+
fn() {
|
|
78
|
+
return this.canAuthorize && !!this.config.client_id;
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Indicates if this token can be refreshed. `true` iff
|
|
84
|
+
* {@link Token@refresh_token} is defined and
|
|
85
|
+
* {@link config.credentials.refreshCallback()} is defined
|
|
86
|
+
*
|
|
87
|
+
* Note: since {@link config} is not evented, we can't listen for changes to
|
|
88
|
+
* {@link config.credentials.refreshCallback()}. As such,
|
|
89
|
+
* {@link config.credentials.refreshCallback()} must always be set before
|
|
90
|
+
* instantiating a {@link Token}
|
|
91
|
+
* @instance
|
|
92
|
+
* @memberof Token
|
|
93
|
+
* @readonly
|
|
94
|
+
* @type {boolean}
|
|
95
|
+
*/
|
|
96
|
+
canRefresh: {
|
|
97
|
+
deps: ['refresh_token'],
|
|
98
|
+
fn() {
|
|
99
|
+
if (inBrowser) {
|
|
100
|
+
return !!this.refresh_token && !!this.config.refreshCallback;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return !!this.refresh_token && !!this.config.client_secret;
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Indicates if this `Token` is expired. `true` iff {@link Token#expires} is
|
|
109
|
+
* defined and is less than {@link Date.now()}.
|
|
110
|
+
* @instance
|
|
111
|
+
* @memberof Token
|
|
112
|
+
* @readonly
|
|
113
|
+
* @type {boolean}
|
|
114
|
+
*/
|
|
115
|
+
isExpired: {
|
|
116
|
+
deps: ['expires', '_isExpired'],
|
|
117
|
+
fn() {
|
|
118
|
+
// in order to avoid setting `cache:false`, we'll use a private property
|
|
119
|
+
// and a timer rather than comparing to `Date.now()`;
|
|
120
|
+
return !!this.expires && this._isExpired;
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cache for toString()
|
|
126
|
+
* @instance
|
|
127
|
+
* @memberof Token
|
|
128
|
+
* @private
|
|
129
|
+
* @readonly
|
|
130
|
+
* @type {string}
|
|
131
|
+
*/
|
|
132
|
+
_string: {
|
|
133
|
+
deps: ['access_token', 'token_type'],
|
|
134
|
+
fn() {
|
|
135
|
+
if (!this.access_token || !this.token_type) {
|
|
136
|
+
return '';
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return `${this.token_type} ${this.access_token}`;
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
namespace: 'Credentials',
|
|
145
|
+
|
|
146
|
+
props: {
|
|
147
|
+
/**
|
|
148
|
+
* Used for indexing in the credentials userTokens collection
|
|
149
|
+
* @instance
|
|
150
|
+
* @memberof Token
|
|
151
|
+
* @private
|
|
152
|
+
* @type {string}
|
|
153
|
+
*/
|
|
154
|
+
scope: 'string',
|
|
155
|
+
/**
|
|
156
|
+
* @instance
|
|
157
|
+
* @memberof Token
|
|
158
|
+
* @type {string}
|
|
159
|
+
*/
|
|
160
|
+
access_token: 'string',
|
|
161
|
+
/**
|
|
162
|
+
* @instance
|
|
163
|
+
* @memberof Token
|
|
164
|
+
* @type {number}
|
|
165
|
+
*/
|
|
166
|
+
expires: 'number',
|
|
167
|
+
/**
|
|
168
|
+
* @instance
|
|
169
|
+
* @memberof Token
|
|
170
|
+
* @type {number}
|
|
171
|
+
*/
|
|
172
|
+
expires_in: 'number',
|
|
173
|
+
/**
|
|
174
|
+
* @instance
|
|
175
|
+
* @memberof Token
|
|
176
|
+
* @type {string}
|
|
177
|
+
*/
|
|
178
|
+
refresh_token: 'string',
|
|
179
|
+
/**
|
|
180
|
+
* @instance
|
|
181
|
+
* @memberof Token
|
|
182
|
+
* @type {number}
|
|
183
|
+
*/
|
|
184
|
+
refresh_token_expires: 'number',
|
|
185
|
+
/**
|
|
186
|
+
* @instance
|
|
187
|
+
* @memberof Token
|
|
188
|
+
* @type {number}
|
|
189
|
+
*/
|
|
190
|
+
refresh_token_expires_in: 'number',
|
|
191
|
+
/**
|
|
192
|
+
* @default "Bearer"
|
|
193
|
+
* @instance
|
|
194
|
+
* @memberof Token
|
|
195
|
+
* @type {string}
|
|
196
|
+
*/
|
|
197
|
+
token_type: {
|
|
198
|
+
default: 'Bearer',
|
|
199
|
+
type: 'string',
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
|
|
203
|
+
session: {
|
|
204
|
+
/**
|
|
205
|
+
* Used by {@link Token#isExpired} to avoid doing a Date comparison.
|
|
206
|
+
* @instance
|
|
207
|
+
* @memberof Token
|
|
208
|
+
* @private
|
|
209
|
+
* @type {boolean}
|
|
210
|
+
*/
|
|
211
|
+
_isExpired: {
|
|
212
|
+
default: false,
|
|
213
|
+
type: 'boolean',
|
|
214
|
+
},
|
|
215
|
+
/**
|
|
216
|
+
* Handle to the previous token that we'll revoke when we refresh this
|
|
217
|
+
* token. The idea is to keep allow two valid tokens when a refresh occurs;
|
|
218
|
+
* we don't want revoke a token that's in the middle of being used, so when
|
|
219
|
+
* we do a token refresh, we won't revoke the token being refreshed, but
|
|
220
|
+
* we'll revoke the previous one.
|
|
221
|
+
* @instance
|
|
222
|
+
* @memberof Token
|
|
223
|
+
* @private
|
|
224
|
+
* @type {Object}
|
|
225
|
+
*/
|
|
226
|
+
previousToken: {
|
|
227
|
+
type: 'state',
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
|
|
231
|
+
@oneFlight({
|
|
232
|
+
keyFactory(scope) {
|
|
233
|
+
return scope;
|
|
234
|
+
},
|
|
235
|
+
})
|
|
236
|
+
/**
|
|
237
|
+
* Uses this token to request a new Token with a subset of this Token's scopes
|
|
238
|
+
* @instance
|
|
239
|
+
* @memberof Token
|
|
240
|
+
* @param {string} scope
|
|
241
|
+
* @returns {Promise<Token>}
|
|
242
|
+
*/
|
|
243
|
+
downscope(scope) {
|
|
244
|
+
this.logger.info(`token: downscoping token to ${scope}`);
|
|
245
|
+
|
|
246
|
+
if (this.isExpired) {
|
|
247
|
+
this.logger.info('token: request received to downscope expired access_token');
|
|
248
|
+
|
|
249
|
+
return Promise.reject(new Error('cannot downscope expired access token'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (!this.canDownscope) {
|
|
253
|
+
if (this.config.client_id) {
|
|
254
|
+
this.logger.info('token: request received to downscope invalid access_token');
|
|
255
|
+
} else {
|
|
256
|
+
this.logger.trace('token: cannot downscope without client_id');
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return Promise.reject(new Error('cannot downscope access token'));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Since we're going to use scope as the index in our token collection, it's
|
|
263
|
+
// important scopes are always deterministically specified.
|
|
264
|
+
if (scope) {
|
|
265
|
+
scope = sortScope(scope);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Ideally, we could depend on the service to communicate this error, but
|
|
269
|
+
// all we get is "invalid scope", which, to the lay person, implies
|
|
270
|
+
// something wrong with *one* of the scopes, not the whole thing.
|
|
271
|
+
if (scope === sortScope(this.config.scope)) {
|
|
272
|
+
return Promise.reject(new Error('token: scope reduction requires a reduced scope'));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return this.webex
|
|
276
|
+
.request({
|
|
277
|
+
method: 'POST',
|
|
278
|
+
uri: this.config.tokenUrl,
|
|
279
|
+
addAuthHeader: false,
|
|
280
|
+
form: {
|
|
281
|
+
grant_type: 'urn:cisco:oauth:grant-type:scope-reduction',
|
|
282
|
+
token: this.access_token,
|
|
283
|
+
scope,
|
|
284
|
+
client_id: this.config.client_id,
|
|
285
|
+
self_contained_token: true,
|
|
286
|
+
},
|
|
287
|
+
})
|
|
288
|
+
.then((res) => {
|
|
289
|
+
this.logger.info(`token: downscoped token to ${scope}`);
|
|
290
|
+
|
|
291
|
+
return new Token(Object.assign(res.body, {scope}), {parent: this.parent});
|
|
292
|
+
});
|
|
293
|
+
},
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Initializer
|
|
297
|
+
* @instance
|
|
298
|
+
* @memberof Token
|
|
299
|
+
* @param {Object} [attrs={}]
|
|
300
|
+
* @param {Object} [options={}]
|
|
301
|
+
* @see {@link WebexPlugin#initialize()}
|
|
302
|
+
* @returns {Token}
|
|
303
|
+
*/
|
|
304
|
+
initialize(attrs = {}, options = {}) {
|
|
305
|
+
Reflect.apply(WebexPlugin.prototype.initialize, this, [attrs, options]);
|
|
306
|
+
|
|
307
|
+
if (typeof attrs === 'string') {
|
|
308
|
+
this.access_token = attrs;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (!this.access_token) {
|
|
312
|
+
throw new Error('`access_token` is required');
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// We don't want the derived property `isExpired` to need {cache:false}, so
|
|
316
|
+
// we'll set up a timer the runs when this token should expire.
|
|
317
|
+
if (this.expires) {
|
|
318
|
+
if (this.expires < Date.now()) {
|
|
319
|
+
this._isExpired = true;
|
|
320
|
+
} else {
|
|
321
|
+
safeSetTimeout(() => {
|
|
322
|
+
this._isExpired = true;
|
|
323
|
+
}, this.expires - Date.now());
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
@oneFlight
|
|
329
|
+
/**
|
|
330
|
+
* Refreshes this Token. Relies on
|
|
331
|
+
* {@link config.credentials.refreshCallback()}
|
|
332
|
+
* @instance
|
|
333
|
+
* @memberof Token
|
|
334
|
+
* @returns {Promise<Token>}
|
|
335
|
+
*/
|
|
336
|
+
refresh() {
|
|
337
|
+
if (!this.canRefresh) {
|
|
338
|
+
throw new Error('Not enough information available to refresh this access token');
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let promise;
|
|
342
|
+
|
|
343
|
+
if (inBrowser) {
|
|
344
|
+
if (!this.config.refreshCallback) {
|
|
345
|
+
throw new Error('Cannot refresh access token without refreshCallback');
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
promise = Promise.resolve(this.config.refreshCallback(this.webex, this));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return (
|
|
352
|
+
promise ||
|
|
353
|
+
this.webex
|
|
354
|
+
.request({
|
|
355
|
+
method: 'POST',
|
|
356
|
+
uri: this.config.tokenUrl,
|
|
357
|
+
form: {
|
|
358
|
+
grant_type: 'refresh_token',
|
|
359
|
+
redirect_uri: this.config.redirect_uri,
|
|
360
|
+
refresh_token: this.refresh_token,
|
|
361
|
+
},
|
|
362
|
+
auth: {
|
|
363
|
+
user: this.config.client_id,
|
|
364
|
+
pass: this.config.client_secret,
|
|
365
|
+
sendImmediately: true,
|
|
366
|
+
},
|
|
367
|
+
shouldRefreshAccessToken: false,
|
|
368
|
+
})
|
|
369
|
+
.then((res) => res.body)
|
|
370
|
+
)
|
|
371
|
+
.then((obj) => {
|
|
372
|
+
if (!obj) {
|
|
373
|
+
throw new Error('token: refreshCallback() did not produce an object');
|
|
374
|
+
}
|
|
375
|
+
// If the authentication server did not send back a refresh token, copy
|
|
376
|
+
// the current refresh token and related values to the response (note:
|
|
377
|
+
// at time of implementation, CI never sends a new refresh token)
|
|
378
|
+
if (!obj.refresh_token) {
|
|
379
|
+
Object.assign(
|
|
380
|
+
obj,
|
|
381
|
+
pick(this, 'refresh_token', 'refresh_token_expires', 'refresh_token_expires_in')
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// If the new token is the same as the previous token, then we may have
|
|
386
|
+
// found a bug in CI; log the details and reject the Promise
|
|
387
|
+
if (this.access_token === obj.access_token) {
|
|
388
|
+
this.logger.error('token: new token matches current token');
|
|
389
|
+
// log the tokens if it is not production
|
|
390
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
391
|
+
this.logger.error('token: current token:', this.access_token);
|
|
392
|
+
this.logger.error('token: new token:', obj.access_token);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
return Promise.reject(new Error('new token matches current token'));
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (this.previousToken) {
|
|
399
|
+
this.previousToken.revoke();
|
|
400
|
+
this.unset('previousToken');
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
obj.previousToken = this;
|
|
404
|
+
obj.scope = this.scope;
|
|
405
|
+
|
|
406
|
+
return new Token(obj, {parent: this.parent});
|
|
407
|
+
})
|
|
408
|
+
.catch(processGrantError);
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
@oneFlight
|
|
412
|
+
/**
|
|
413
|
+
* Revokes this token and unsets its local properties
|
|
414
|
+
* @instance
|
|
415
|
+
* @memberof Token
|
|
416
|
+
* @returns {Promise}
|
|
417
|
+
*/
|
|
418
|
+
revoke() {
|
|
419
|
+
if (this.isExpired) {
|
|
420
|
+
this.logger.info('token: already expired, not making making revocation request');
|
|
421
|
+
|
|
422
|
+
return Promise.resolve();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
if (!this.canAuthorize) {
|
|
426
|
+
this.logger.info('token: no longer valid, not making revocation request');
|
|
427
|
+
|
|
428
|
+
return Promise.resolve();
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// FIXME we need to use the user token revocation endpoint to revoke a token
|
|
432
|
+
// without a client_secret, but it doesn't current support using a token to
|
|
433
|
+
// revoke itself
|
|
434
|
+
// Note: I'm not making a canRevoke property because there should be changes
|
|
435
|
+
// coming to the user token revocation endpoint that allow us to do this
|
|
436
|
+
// correctly.
|
|
437
|
+
if (!this.config.client_secret) {
|
|
438
|
+
this.logger.info('token: no client secret available, not making revocation request');
|
|
439
|
+
|
|
440
|
+
return Promise.resolve();
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
this.logger.info('token: revoking access token');
|
|
444
|
+
|
|
445
|
+
return this.webex
|
|
446
|
+
.request({
|
|
447
|
+
method: 'POST',
|
|
448
|
+
uri: this.config.revokeUrl,
|
|
449
|
+
form: {
|
|
450
|
+
token: this.access_token,
|
|
451
|
+
token_type_hint: 'access_token',
|
|
452
|
+
},
|
|
453
|
+
auth: {
|
|
454
|
+
user: this.config.client_id,
|
|
455
|
+
pass: this.config.client_secret,
|
|
456
|
+
sendImmediately: true,
|
|
457
|
+
},
|
|
458
|
+
shouldRefreshAccessToken: false,
|
|
459
|
+
})
|
|
460
|
+
.then(() => {
|
|
461
|
+
this.unset(['access_token', 'expires', 'expires_in', 'token_type']);
|
|
462
|
+
this.logger.info('token: access token revoked');
|
|
463
|
+
})
|
|
464
|
+
.catch(processGrantError);
|
|
465
|
+
},
|
|
466
|
+
|
|
467
|
+
set(...args) {
|
|
468
|
+
// eslint-disable-next-line prefer-const
|
|
469
|
+
let [attrs, options] = this._filterSetParameters(...args);
|
|
470
|
+
|
|
471
|
+
if (!attrs.token_type && attrs.access_token && attrs.access_token.includes(' ')) {
|
|
472
|
+
const [token_type, access_token] = attrs.access_token.split(' ');
|
|
473
|
+
|
|
474
|
+
attrs = {...attrs, access_token, token_type};
|
|
475
|
+
}
|
|
476
|
+
const now = Date.now();
|
|
477
|
+
|
|
478
|
+
if (!attrs.expires && attrs.expires_in) {
|
|
479
|
+
attrs.expires = now + attrs.expires_in * 1000;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
if (!attrs.refresh_token_expires && attrs.refresh_token_expires_in) {
|
|
483
|
+
attrs.refresh_token_expires = now + attrs.refresh_token_expires_in * 1000;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (attrs.scope) {
|
|
487
|
+
attrs.scope = sortScope(attrs.scope);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
return Reflect.apply(WebexPlugin.prototype.set, this, [attrs, options]);
|
|
491
|
+
},
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Renders the token object as an HTTP Header Value
|
|
495
|
+
* @instance
|
|
496
|
+
* @memberof Token
|
|
497
|
+
* @returns {string}
|
|
498
|
+
* @see {@link Object#toString()}
|
|
499
|
+
*/
|
|
500
|
+
toString() {
|
|
501
|
+
if (!this._string) {
|
|
502
|
+
throw new Error('cannot stringify Token');
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return this._string;
|
|
506
|
+
},
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Uses a non-producation api to return information about this token. This
|
|
510
|
+
* method is primarily for tests and will throw if NODE_ENV === production
|
|
511
|
+
* @instance
|
|
512
|
+
* @memberof Token
|
|
513
|
+
* @private
|
|
514
|
+
* @returns {Promise}
|
|
515
|
+
*/
|
|
516
|
+
validate() {
|
|
517
|
+
if (process.env.NODE_ENV === 'production') {
|
|
518
|
+
throw new Error('Token#validate() must not be used in production');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
return this.webex
|
|
522
|
+
.request({
|
|
523
|
+
method: 'POST',
|
|
524
|
+
service: 'conversation',
|
|
525
|
+
resource: 'users/validateAuthToken',
|
|
526
|
+
body: {
|
|
527
|
+
token: this.access_token,
|
|
528
|
+
},
|
|
529
|
+
})
|
|
530
|
+
.catch((reason) => {
|
|
531
|
+
if ('statusCode' in reason) {
|
|
532
|
+
return Promise.reject(reason);
|
|
533
|
+
}
|
|
534
|
+
this.logger.info("REMINDER: If you're investigating a network error here, it's normal");
|
|
535
|
+
|
|
536
|
+
// If we got an error that isn't a WebexHttpError, assume the problem is
|
|
537
|
+
// that we don't have the wdm plugin loaded and service/resource isn't
|
|
538
|
+
// a valid means of identifying a request.
|
|
539
|
+
const convApi =
|
|
540
|
+
process.env.CONVERSATION_SERVICE ||
|
|
541
|
+
process.env.CONVERSATION_SERVICE_URL ||
|
|
542
|
+
'https://conv-a.wbx2.com/conversation/api/v1';
|
|
543
|
+
|
|
544
|
+
return this.webex.request({
|
|
545
|
+
method: 'POST',
|
|
546
|
+
uri: `${convApi}/users/validateAuthToken`,
|
|
547
|
+
body: {
|
|
548
|
+
token: this.access_token,
|
|
549
|
+
},
|
|
550
|
+
headers: {
|
|
551
|
+
authorization: `Bearer ${this.access_token}`,
|
|
552
|
+
},
|
|
553
|
+
});
|
|
554
|
+
})
|
|
555
|
+
.then((res) => res.body);
|
|
556
|
+
},
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
export default Token;
|