@webex/webex-core 2.59.3-next.1 → 2.59.4
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 +19 -20
- 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,1017 +1,1017 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import {set} from 'lodash';
|
|
6
|
-
import {assert} from '@webex/test-helper-chai';
|
|
7
|
-
import sinon from 'sinon';
|
|
8
|
-
import MockWebex from '@webex/test-helper-mock-webex';
|
|
9
|
-
import {Credentials, Token, grantErrors} from '@webex/webex-core';
|
|
10
|
-
import {inBrowser} from '@webex/common';
|
|
11
|
-
import FakeTimers from '@sinonjs/fake-timers';
|
|
12
|
-
import {skipInBrowser} from '@webex/test-helper-mocha';
|
|
13
|
-
import Logger from '@webex/plugin-logger';
|
|
14
|
-
|
|
15
|
-
/* eslint camelcase: [0] */
|
|
16
|
-
|
|
17
|
-
// eslint-disable-next-line no-empty-function
|
|
18
|
-
function noop() {}
|
|
19
|
-
|
|
20
|
-
function promiseTick(count) {
|
|
21
|
-
let promise = Promise.resolve();
|
|
22
|
-
|
|
23
|
-
while (count > 1) {
|
|
24
|
-
promise = promise.then(() => promiseTick(1));
|
|
25
|
-
count -= 1;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return promise;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const AUTHORIZATION_STRING =
|
|
32
|
-
'https://api.ciscospark.com/v1/authorize?client_id=MOCK_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000&scope=spark%3Arooms_read%20spark%3Ateams_read&state=set_state_here';
|
|
33
|
-
|
|
34
|
-
describe('webex-core', () => {
|
|
35
|
-
describe('Credentials', () => {
|
|
36
|
-
let clock;
|
|
37
|
-
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
clock = FakeTimers.install({now: Date.now()});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
afterEach(() => {
|
|
43
|
-
clock.uninstall();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
function makeToken(webex, options) {
|
|
47
|
-
return new Token(options, {parent: webex});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
describe('#calcRefreshTimeout', () => {
|
|
51
|
-
it('generates a number between 60-90% of expiration', () => {
|
|
52
|
-
const expiration = 1000;
|
|
53
|
-
const webex = new MockWebex();
|
|
54
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
55
|
-
const result = credentials.calcRefreshTimeout(expiration);
|
|
56
|
-
|
|
57
|
-
assert.isTrue(result >= expiration * 0.6);
|
|
58
|
-
assert.isTrue(result <= expiration * 0.9);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
describe('#canAuthorize', () => {
|
|
63
|
-
it('indicates if the current state has enough information to populate an auth header, even if a token refresh or token downscope is required', () => {
|
|
64
|
-
const webex = new MockWebex();
|
|
65
|
-
|
|
66
|
-
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
67
|
-
let credentials = new Credentials(undefined, {parent: webex});
|
|
68
|
-
|
|
69
|
-
webex.trigger('change:config');
|
|
70
|
-
assert.isFalse(credentials.canAuthorize);
|
|
71
|
-
|
|
72
|
-
credentials.supertoken = makeToken(webex, {
|
|
73
|
-
access_token: 'AT',
|
|
74
|
-
});
|
|
75
|
-
assert.isTrue(credentials.canAuthorize);
|
|
76
|
-
|
|
77
|
-
credentials.supertoken.unset('access_token');
|
|
78
|
-
assert.isFalse(credentials.canAuthorize);
|
|
79
|
-
|
|
80
|
-
credentials.supertoken = makeToken(webex, {
|
|
81
|
-
access_token: 'AT',
|
|
82
|
-
});
|
|
83
|
-
assert.isTrue(credentials.canAuthorize);
|
|
84
|
-
|
|
85
|
-
credentials.supertoken = makeToken(webex, {
|
|
86
|
-
access_token: 'AT',
|
|
87
|
-
expires: Date.now() - 10000,
|
|
88
|
-
});
|
|
89
|
-
assert.isFalse(credentials.supertoken.canAuthorize);
|
|
90
|
-
assert.isFalse(credentials.canRefresh);
|
|
91
|
-
assert.isFalse(credentials.canAuthorize);
|
|
92
|
-
|
|
93
|
-
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
94
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
95
|
-
webex.trigger('change:config');
|
|
96
|
-
assert.isFalse(credentials.canAuthorize);
|
|
97
|
-
credentials.supertoken = makeToken(webex, {
|
|
98
|
-
access_token: 'AT',
|
|
99
|
-
refresh_token: 'RT',
|
|
100
|
-
});
|
|
101
|
-
credentials.supertoken.unset('access_token');
|
|
102
|
-
assert.isTrue(credentials.canAuthorize);
|
|
103
|
-
});
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
describe('#canRefresh', () => {
|
|
107
|
-
it('indicates if there is presently enough information to refresh', () => {
|
|
108
|
-
const webex = new MockWebex();
|
|
109
|
-
let credentials = new Credentials(undefined, {parent: webex});
|
|
110
|
-
|
|
111
|
-
webex.trigger('change:config');
|
|
112
|
-
assert.isFalse(credentials.canRefresh);
|
|
113
|
-
credentials.supertoken = makeToken(
|
|
114
|
-
webex,
|
|
115
|
-
{
|
|
116
|
-
access_token: 'AT',
|
|
117
|
-
},
|
|
118
|
-
{parent: true}
|
|
119
|
-
);
|
|
120
|
-
assert.isFalse(credentials.canRefresh);
|
|
121
|
-
|
|
122
|
-
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
123
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
124
|
-
webex.trigger('change:config');
|
|
125
|
-
assert.isFalse(credentials.canRefresh);
|
|
126
|
-
credentials.supertoken = makeToken(webex, {
|
|
127
|
-
access_token: 'AT',
|
|
128
|
-
refresh_token: 'RT',
|
|
129
|
-
});
|
|
130
|
-
assert.isTrue(credentials.supertoken.canRefresh);
|
|
131
|
-
assert.isTrue(credentials.canRefresh);
|
|
132
|
-
});
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
describe('#buildLoginUrl()', () => {
|
|
136
|
-
it('requires `state` to be an object', () => {
|
|
137
|
-
const webex = new MockWebex({
|
|
138
|
-
children: {
|
|
139
|
-
credentials: Credentials,
|
|
140
|
-
},
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
webex.trigger('change:config)');
|
|
144
|
-
assert.doesNotThrow(() => {
|
|
145
|
-
webex.credentials.buildLoginUrl();
|
|
146
|
-
}, /if specified, `options.state` must be an object/);
|
|
147
|
-
|
|
148
|
-
assert.doesNotThrow(() => {
|
|
149
|
-
webex.credentials.buildLoginUrl({});
|
|
150
|
-
}, /if specified, `options.state` must be an object/);
|
|
151
|
-
|
|
152
|
-
assert.throws(() => {
|
|
153
|
-
webex.credentials.buildLoginUrl({state: 'state'});
|
|
154
|
-
}, /if specified, `options.state` must be an object/);
|
|
155
|
-
|
|
156
|
-
assert.doesNotThrow(() => {
|
|
157
|
-
webex.credentials.buildLoginUrl({state: {}});
|
|
158
|
-
}, /if specified, `options.state` must be an object/);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('prefers the hydra auth url, but falls back to idbroker', () => {
|
|
162
|
-
const webex = new MockWebex();
|
|
163
|
-
let credentials = new Credentials(undefined, {parent: webex});
|
|
164
|
-
|
|
165
|
-
webex.trigger('change:config');
|
|
166
|
-
|
|
167
|
-
assert.match(credentials.buildLoginUrl({state: {}}), /idbroker/);
|
|
168
|
-
webex.config.credentials = {
|
|
169
|
-
authorizationString: AUTHORIZATION_STRING,
|
|
170
|
-
};
|
|
171
|
-
credentials = new Credentials({}, {parent: webex});
|
|
172
|
-
webex.trigger('change:config');
|
|
173
|
-
assert.match(credentials.buildLoginUrl({state: {}}), /api.ciscospark.com/);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
skipInBrowser(it)('generates the login url', () => {
|
|
177
|
-
const webex = new MockWebex();
|
|
178
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
179
|
-
|
|
180
|
-
webex.trigger('change:config');
|
|
181
|
-
|
|
182
|
-
assert.equal(
|
|
183
|
-
credentials.buildLoginUrl({state: {page: 'front'}}),
|
|
184
|
-
`${
|
|
185
|
-
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
186
|
-
}/idb/oauth2/v1/authorize?state=eyJwYWdlIjoiZnJvbnQifQ&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
|
|
187
|
-
);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
skipInBrowser(it)('generates the login url with empty state param', () => {
|
|
191
|
-
const webex = new MockWebex();
|
|
192
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
193
|
-
|
|
194
|
-
webex.trigger('change:config');
|
|
195
|
-
|
|
196
|
-
assert.equal(
|
|
197
|
-
credentials.buildLoginUrl({state: {}}),
|
|
198
|
-
`${
|
|
199
|
-
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
200
|
-
}/idb/oauth2/v1/authorize?client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
|
|
201
|
-
);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
describe('#buildLogoutUrl()', () => {
|
|
206
|
-
skipInBrowser(it)('generates the logout url', () => {
|
|
207
|
-
const webex = new MockWebex();
|
|
208
|
-
|
|
209
|
-
webex.config.credentials.redirect_uri = 'ru';
|
|
210
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
211
|
-
|
|
212
|
-
webex.trigger('change:config');
|
|
213
|
-
assert.equal(
|
|
214
|
-
credentials.buildLogoutUrl(),
|
|
215
|
-
`${
|
|
216
|
-
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
217
|
-
}/idb/oauth2/v1/logout?cisService=webex&goto=ru`
|
|
218
|
-
);
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
skipInBrowser(it)('includes a token param if passed', () => {
|
|
222
|
-
const webex = new MockWebex();
|
|
223
|
-
|
|
224
|
-
webex.config.credentials.redirect_uri = 'ru';
|
|
225
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
226
|
-
|
|
227
|
-
webex.trigger('change:config');
|
|
228
|
-
assert.equal(
|
|
229
|
-
credentials.buildLogoutUrl({token: 't'}),
|
|
230
|
-
`${
|
|
231
|
-
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
232
|
-
}/idb/oauth2/v1/logout?cisService=webex&goto=ru&token=t`
|
|
233
|
-
);
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('always fallsback to idbroker', () => {
|
|
237
|
-
const webex = new MockWebex();
|
|
238
|
-
|
|
239
|
-
webex.config.credentials.redirect_uri = 'ru';
|
|
240
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
241
|
-
|
|
242
|
-
webex.trigger('change:config');
|
|
243
|
-
assert.match(credentials.buildLogoutUrl(), /idbroker.*?goto=ru/);
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('allows overriding the goto url', () => {
|
|
247
|
-
const webex = new MockWebex();
|
|
248
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
249
|
-
|
|
250
|
-
webex.trigger('change:config');
|
|
251
|
-
assert.match(
|
|
252
|
-
credentials.buildLogoutUrl({goto: 'http://example.com/'}),
|
|
253
|
-
/goto=http%3A%2F%2Fexample.com%2F/
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
describe('#getOrgId()', () => {
|
|
259
|
-
let credentials;
|
|
260
|
-
let orgId;
|
|
261
|
-
let webex;
|
|
262
|
-
|
|
263
|
-
beforeEach(() => {
|
|
264
|
-
webex = new MockWebex();
|
|
265
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it('should return the OrgId of JWT-authenticated user', () => {
|
|
269
|
-
credentials.supertoken = {
|
|
270
|
-
access_token:
|
|
271
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk',
|
|
272
|
-
};
|
|
273
|
-
|
|
274
|
-
orgId = 'my-realm';
|
|
275
|
-
|
|
276
|
-
assert.equal(credentials.getOrgId(), orgId);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('should return the OrgId of a user-token-authenticated user', () => {
|
|
280
|
-
orgId = 'this-is-an-org-id';
|
|
281
|
-
|
|
282
|
-
credentials.supertoken = {
|
|
283
|
-
access_token: `000_000_${orgId}`,
|
|
284
|
-
};
|
|
285
|
-
|
|
286
|
-
assert.equal(credentials.getOrgId(), orgId);
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should throw if the OrgId was not determined', () =>
|
|
290
|
-
expect(() => credentials.getOrgId()).toThrow('the provided token is not a valid format'));
|
|
291
|
-
});
|
|
292
|
-
|
|
293
|
-
describe('#extractOrgIdFromJWT()', () => {
|
|
294
|
-
let credentials;
|
|
295
|
-
let webex;
|
|
296
|
-
|
|
297
|
-
beforeEach(() => {
|
|
298
|
-
webex = new MockWebex();
|
|
299
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it('should return the OrgId of a provided JWT', () => {
|
|
303
|
-
const jwt =
|
|
304
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk';
|
|
305
|
-
const realm = 'my-realm';
|
|
306
|
-
|
|
307
|
-
assert.equal(credentials.extractOrgIdFromJWT(jwt), realm);
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('should throw if the provided JWT is not valid', () =>
|
|
311
|
-
expect(() => credentials.extractOrgIdFromJWT('not-valid')).toThrow());
|
|
312
|
-
|
|
313
|
-
it('should throw if the provided JWT does not contain an OrgId', () => {
|
|
314
|
-
const jwtNoOrg =
|
|
315
|
-
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
316
|
-
|
|
317
|
-
expect(() => credentials.extractOrgIdFromJWT(jwtNoOrg)).toThrow();
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('should throw if no JWT was provided', () =>
|
|
321
|
-
expect(() => credentials.extractOrgIdFromJWT()).toThrow());
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
describe('#extractOrgIdFromUserToken()', () => {
|
|
325
|
-
let credentials;
|
|
326
|
-
let webex;
|
|
327
|
-
|
|
328
|
-
beforeEach(() => {
|
|
329
|
-
webex = new MockWebex();
|
|
330
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
331
|
-
});
|
|
332
|
-
|
|
333
|
-
it('should return the OrgId of the provided user token', () => {
|
|
334
|
-
const orgId = 'this-is-an-org-id';
|
|
335
|
-
const userToken = `000_000_${orgId}`;
|
|
336
|
-
|
|
337
|
-
assert.equal(credentials.extractOrgIdFromUserToken(userToken), orgId);
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('should throw when provided an invalid token', () =>
|
|
341
|
-
expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format'));
|
|
342
|
-
|
|
343
|
-
it('should throw when no token is provided', () =>
|
|
344
|
-
expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe('#initialize()', () => {
|
|
348
|
-
it('turns a portal auth string into a configuration object', () => {
|
|
349
|
-
const webex = new MockWebex();
|
|
350
|
-
|
|
351
|
-
webex.config.credentials = {
|
|
352
|
-
client_id: 'ci',
|
|
353
|
-
redirect_uri: 'ru',
|
|
354
|
-
scope: 's',
|
|
355
|
-
};
|
|
356
|
-
|
|
357
|
-
let credentials = new Credentials(undefined, {parent: webex});
|
|
358
|
-
|
|
359
|
-
webex.trigger('change:config');
|
|
360
|
-
webex.trigger('change:config');
|
|
361
|
-
assert.equal(webex.config.credentials.client_id, 'ci');
|
|
362
|
-
assert.equal(credentials.config.client_id, 'ci');
|
|
363
|
-
assert.equal(webex.config.credentials.redirect_uri, 'ru');
|
|
364
|
-
assert.equal(credentials.config.redirect_uri, 'ru');
|
|
365
|
-
assert.equal(webex.config.credentials.scope, 's');
|
|
366
|
-
assert.equal(credentials.config.scope, 's');
|
|
367
|
-
|
|
368
|
-
// Accept a portal auth string via environment variables
|
|
369
|
-
webex.config.credentials.authorizationString = AUTHORIZATION_STRING;
|
|
370
|
-
credentials = new Credentials(undefined, {parent: webex});
|
|
371
|
-
webex.trigger('change:config');
|
|
372
|
-
webex.trigger('change:config');
|
|
373
|
-
assert.equal(webex.config.credentials.client_id, 'MOCK_CLIENT_ID');
|
|
374
|
-
assert.equal(credentials.config.client_id, 'MOCK_CLIENT_ID');
|
|
375
|
-
assert.equal(webex.config.credentials.redirect_uri, 'http://localhost:8000');
|
|
376
|
-
assert.equal(credentials.config.redirect_uri, 'http://localhost:8000');
|
|
377
|
-
assert.equal(webex.config.credentials.scope, 'spark:rooms_read spark:teams_read');
|
|
378
|
-
assert.equal(credentials.config.scope, 'spark:rooms_read spark:teams_read');
|
|
379
|
-
});
|
|
380
|
-
|
|
381
|
-
[
|
|
382
|
-
'data',
|
|
383
|
-
'data.access_token',
|
|
384
|
-
'data.supertoken',
|
|
385
|
-
'data.supertoken.access_token',
|
|
386
|
-
'data.authorization',
|
|
387
|
-
'data.authorization.supertoken',
|
|
388
|
-
'data.authorization.supertoken.access_token',
|
|
389
|
-
]
|
|
390
|
-
.reduce(
|
|
391
|
-
(acc, path) =>
|
|
392
|
-
acc.concat(
|
|
393
|
-
['ST', 'Bearer ST'].map((str) => {
|
|
394
|
-
const obj = {
|
|
395
|
-
msg: `accepts token string "${str}" at path "${path
|
|
396
|
-
.split('.')
|
|
397
|
-
.slice(1)
|
|
398
|
-
.join('.')}"`,
|
|
399
|
-
};
|
|
400
|
-
|
|
401
|
-
set(obj, path, str);
|
|
402
|
-
|
|
403
|
-
return obj;
|
|
404
|
-
})
|
|
405
|
-
),
|
|
406
|
-
[]
|
|
407
|
-
)
|
|
408
|
-
.forEach(({msg, data}) => {
|
|
409
|
-
it(msg, () => {
|
|
410
|
-
const webex = new MockWebex();
|
|
411
|
-
const credentials = new Credentials(data, {parent: webex});
|
|
412
|
-
|
|
413
|
-
assert.isTrue(credentials.canAuthorize);
|
|
414
|
-
assert.equal(credentials.supertoken.access_token, 'ST');
|
|
415
|
-
assert.equal(credentials.supertoken.token_type, 'Bearer');
|
|
416
|
-
});
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it('schedules a refreshTimer', () => {
|
|
420
|
-
const webex = new MockWebex();
|
|
421
|
-
const supertoken = makeToken(webex, {
|
|
422
|
-
access_token: 'ST',
|
|
423
|
-
refresh_token: 'RT',
|
|
424
|
-
expires: Date.now() + 10000,
|
|
425
|
-
});
|
|
426
|
-
const supertoken2 = makeToken(webex, {
|
|
427
|
-
access_token: 'ST2',
|
|
428
|
-
refresh_token: 'RT2',
|
|
429
|
-
expires: Date.now() + 20000,
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
sinon.stub(supertoken, 'refresh').returns(Promise.resolve(supertoken2));
|
|
433
|
-
const credentials = new Credentials(supertoken, {parent: webex});
|
|
434
|
-
|
|
435
|
-
webex.trigger('change:config');
|
|
436
|
-
|
|
437
|
-
const firstTimer = credentials.refreshTimer;
|
|
438
|
-
|
|
439
|
-
assert.isDefined(firstTimer);
|
|
440
|
-
assert.notCalled(supertoken.refresh);
|
|
441
|
-
clock.tick(10000);
|
|
442
|
-
|
|
443
|
-
return promiseTick(8)
|
|
444
|
-
.then(() => assert.called(supertoken.refresh))
|
|
445
|
-
.then(() => assert.isDefined(credentials.refreshTimer))
|
|
446
|
-
.then(() => assert.notEqual(credentials.refreshTimer, firstTimer));
|
|
447
|
-
});
|
|
448
|
-
|
|
449
|
-
it.skip('does not schedule a refreshTimer', () => {
|
|
450
|
-
const webex = new MockWebex();
|
|
451
|
-
const supertoken = makeToken(webex, {
|
|
452
|
-
access_token: 'ST',
|
|
453
|
-
refresh_token: 'RT',
|
|
454
|
-
expires: Date.now() - 10000,
|
|
455
|
-
});
|
|
456
|
-
|
|
457
|
-
sinon.stub(supertoken, 'refresh').returns(Promise.reject());
|
|
458
|
-
const credentials = new Credentials(supertoken, {parent: webex});
|
|
459
|
-
|
|
460
|
-
webex.trigger('change:config');
|
|
461
|
-
|
|
462
|
-
assert.isUndefined(credentials.refreshTimer);
|
|
463
|
-
});
|
|
464
|
-
});
|
|
465
|
-
|
|
466
|
-
describe('#getUserToken()', () => {
|
|
467
|
-
// it('resolves with the supertoken if the supertoken matches the requested scopes');
|
|
468
|
-
|
|
469
|
-
it('resolves with the token identified by the specified scopes', () => {
|
|
470
|
-
const webex = new MockWebex();
|
|
471
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
472
|
-
|
|
473
|
-
webex.trigger('change:config');
|
|
474
|
-
const st = makeToken(webex, {access_token: 'ST'});
|
|
475
|
-
const t1 = makeToken(webex, {
|
|
476
|
-
access_token: 'AT1',
|
|
477
|
-
scope: 'scope1',
|
|
478
|
-
});
|
|
479
|
-
const t2 = makeToken(webex, {
|
|
480
|
-
access_token: 'AT2',
|
|
481
|
-
scope: 'scope2',
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
credentials.set({
|
|
485
|
-
supertoken: st,
|
|
486
|
-
userTokens: [t1, t2],
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
return Promise.all([
|
|
490
|
-
credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t1)),
|
|
491
|
-
credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, t2)),
|
|
492
|
-
]);
|
|
493
|
-
});
|
|
494
|
-
|
|
495
|
-
describe('when no matching token is found', () => {
|
|
496
|
-
it('downscopes the supertoken', () => {
|
|
497
|
-
const webex = new MockWebex();
|
|
498
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
499
|
-
|
|
500
|
-
webex.trigger('change:config');
|
|
501
|
-
|
|
502
|
-
credentials.supertoken = makeToken(webex, {
|
|
503
|
-
access_token: 'ST',
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
const t2 = makeToken(webex, {
|
|
507
|
-
access_token: 'AT2',
|
|
508
|
-
});
|
|
509
|
-
|
|
510
|
-
sinon.stub(credentials.supertoken, 'downscope').returns(Promise.resolve(t2));
|
|
511
|
-
|
|
512
|
-
const t1 = makeToken(webex, {
|
|
513
|
-
access_token: 'AT1',
|
|
514
|
-
scope: 'scope1',
|
|
515
|
-
});
|
|
516
|
-
|
|
517
|
-
credentials.set({
|
|
518
|
-
userTokens: [t1],
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
return credentials
|
|
522
|
-
.getUserToken('scope2')
|
|
523
|
-
.then((result) => assert.deepEqual(result, t2))
|
|
524
|
-
.then(() => assert.calledWith(credentials.supertoken.downscope, 'scope2'));
|
|
525
|
-
});
|
|
526
|
-
});
|
|
527
|
-
|
|
528
|
-
describe('when no scope is specified', () => {
|
|
529
|
-
it('resolves with a token containing all but the kms scopes', () => {
|
|
530
|
-
const webex = new MockWebex();
|
|
531
|
-
|
|
532
|
-
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
533
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
534
|
-
|
|
535
|
-
webex.trigger('change:config');
|
|
536
|
-
|
|
537
|
-
credentials.supertoken = makeToken(webex, {
|
|
538
|
-
access_token: 'ST',
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// const t2 = makeToken(webex, {
|
|
542
|
-
// access_token: `AT2`
|
|
543
|
-
// });
|
|
544
|
-
|
|
545
|
-
// sinon.stub(credentials.supertoken, `downscope`).returns(Promise.resolve(t2));
|
|
546
|
-
|
|
547
|
-
const t1 = makeToken(webex, {
|
|
548
|
-
access_token: 'AT1',
|
|
549
|
-
scope: 'scope1',
|
|
550
|
-
});
|
|
551
|
-
|
|
552
|
-
credentials.set({
|
|
553
|
-
userTokens: [t1],
|
|
554
|
-
});
|
|
555
|
-
|
|
556
|
-
return credentials.getUserToken().then((result) => assert.deepEqual(result, t1));
|
|
557
|
-
});
|
|
558
|
-
});
|
|
559
|
-
|
|
560
|
-
describe('when the kms downscope request fails', () => {
|
|
561
|
-
it('falls back to the supertoken', () => {
|
|
562
|
-
const webex = new MockWebex({
|
|
563
|
-
children: {
|
|
564
|
-
logger: Logger,
|
|
565
|
-
},
|
|
566
|
-
});
|
|
567
|
-
|
|
568
|
-
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
569
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
570
|
-
|
|
571
|
-
webex.trigger('change:config');
|
|
572
|
-
|
|
573
|
-
credentials.supertoken = makeToken(webex, {
|
|
574
|
-
access_token: 'ST',
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
sinon
|
|
578
|
-
.stub(credentials.supertoken, 'downscope')
|
|
579
|
-
.returns(Promise.reject(new Error('downscope failed')));
|
|
580
|
-
|
|
581
|
-
const t1 = makeToken(webex, {
|
|
582
|
-
access_token: 'AT1',
|
|
583
|
-
scope: 'scope1',
|
|
584
|
-
});
|
|
585
|
-
|
|
586
|
-
credentials.set({
|
|
587
|
-
userTokens: [t1],
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
return credentials
|
|
591
|
-
.getUserToken('scope2')
|
|
592
|
-
.then((t) => assert.equal(t.access_token, credentials.supertoken.access_token));
|
|
593
|
-
});
|
|
594
|
-
});
|
|
595
|
-
|
|
596
|
-
it('is blocked while a token refresh is inflight', () => {
|
|
597
|
-
const webex = new MockWebex();
|
|
598
|
-
|
|
599
|
-
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
600
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
601
|
-
|
|
602
|
-
webex.trigger('change:config');
|
|
603
|
-
|
|
604
|
-
const supertoken1 = makeToken(webex, {
|
|
605
|
-
access_token: 'ST1',
|
|
606
|
-
refresh_token: 'RT1',
|
|
607
|
-
});
|
|
608
|
-
|
|
609
|
-
credentials.set({supertoken: supertoken1});
|
|
610
|
-
|
|
611
|
-
sinon
|
|
612
|
-
.stub(supertoken1, 'downscope')
|
|
613
|
-
.returns(Promise.resolve(new Token({access_token: 'ST1ATD'})));
|
|
614
|
-
const supertoken2 = makeToken(webex, {
|
|
615
|
-
access_token: 'ST2',
|
|
616
|
-
});
|
|
617
|
-
|
|
618
|
-
sinon.stub(supertoken1, 'refresh').returns(Promise.resolve(supertoken2));
|
|
619
|
-
|
|
620
|
-
const at2 = makeToken(webex, {access_token: 'ST2ATD'});
|
|
621
|
-
|
|
622
|
-
sinon.stub(supertoken2, 'downscope').returns(Promise.resolve(at2));
|
|
623
|
-
|
|
624
|
-
return Promise.all([
|
|
625
|
-
credentials.refresh(),
|
|
626
|
-
credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, at2)),
|
|
627
|
-
]);
|
|
628
|
-
});
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
describe('#invalidate()', () => {
|
|
632
|
-
it('clears the refreshTimer', () => {
|
|
633
|
-
const webex = new MockWebex();
|
|
634
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
635
|
-
|
|
636
|
-
webex.trigger('change:config');
|
|
637
|
-
const st = makeToken(webex, {
|
|
638
|
-
access_token: 'ST',
|
|
639
|
-
refresh_token: 'RT',
|
|
640
|
-
});
|
|
641
|
-
|
|
642
|
-
const st2 = makeToken(webex, {
|
|
643
|
-
access_token: 'ST2',
|
|
644
|
-
refresh_token: 'RT2',
|
|
645
|
-
});
|
|
646
|
-
|
|
647
|
-
credentials.set({
|
|
648
|
-
supertoken: st,
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
652
|
-
|
|
653
|
-
credentials.scheduleRefresh(Date.now() + 10000);
|
|
654
|
-
assert.isDefined(credentials.refreshTimer);
|
|
655
|
-
assert.notCalled(credentials.refresh);
|
|
656
|
-
|
|
657
|
-
return credentials.invalidate().then(() => {
|
|
658
|
-
clock.tick(10000);
|
|
659
|
-
assert.isUndefined(credentials.refreshTimer);
|
|
660
|
-
assert.notCalled(credentials.refresh);
|
|
661
|
-
});
|
|
662
|
-
});
|
|
663
|
-
|
|
664
|
-
it('clears the tokens from boundedStorage', () => {
|
|
665
|
-
const webex = new MockWebex();
|
|
666
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
667
|
-
|
|
668
|
-
webex.trigger('change:config');
|
|
669
|
-
const st = makeToken(webex, {
|
|
670
|
-
access_token: 'ST',
|
|
671
|
-
});
|
|
672
|
-
|
|
673
|
-
const t1 = makeToken(webex, {
|
|
674
|
-
access_token: 'AT1',
|
|
675
|
-
scope: 'scope1',
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
const t2 = makeToken(webex, {
|
|
679
|
-
access_token: 'AT2',
|
|
680
|
-
scope: 'scope2',
|
|
681
|
-
});
|
|
682
|
-
|
|
683
|
-
credentials.set({
|
|
684
|
-
supertoken: st,
|
|
685
|
-
userTokens: [t1, t2],
|
|
686
|
-
});
|
|
687
|
-
|
|
688
|
-
return new Promise((resolve) => {
|
|
689
|
-
setTimeout(resolve, 1);
|
|
690
|
-
clock.tick(1000);
|
|
691
|
-
})
|
|
692
|
-
.then(() => webex.boundedStorage.get('Credentials', '@'))
|
|
693
|
-
.then((data) => {
|
|
694
|
-
assert.equal(data.userTokens[0].access_token, t1.access_token);
|
|
695
|
-
assert.equal(data.userTokens[1].access_token, t2.access_token);
|
|
696
|
-
|
|
697
|
-
return credentials.invalidate();
|
|
698
|
-
})
|
|
699
|
-
.then(() => promiseTick(500))
|
|
700
|
-
.then(
|
|
701
|
-
() =>
|
|
702
|
-
new Promise((resolve) => {
|
|
703
|
-
setTimeout(resolve, 1);
|
|
704
|
-
clock.tick(1000);
|
|
705
|
-
})
|
|
706
|
-
)
|
|
707
|
-
.then(() => promiseTick(500))
|
|
708
|
-
.then(
|
|
709
|
-
() =>
|
|
710
|
-
new Promise((resolve) => {
|
|
711
|
-
setTimeout(resolve, 1);
|
|
712
|
-
clock.tick(1000);
|
|
713
|
-
})
|
|
714
|
-
)
|
|
715
|
-
.then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
// it('does not induce any token refreshes');
|
|
720
|
-
|
|
721
|
-
it('prevents #getUserToken() from being invoked', () => {
|
|
722
|
-
const webex = new MockWebex();
|
|
723
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
724
|
-
|
|
725
|
-
webex.trigger('change:config');
|
|
726
|
-
const st = makeToken(webex, {
|
|
727
|
-
access_token: 'ST',
|
|
728
|
-
refresh_token: 'RT',
|
|
729
|
-
});
|
|
730
|
-
|
|
731
|
-
const t1 = makeToken(webex, {
|
|
732
|
-
access_token: 'AT1',
|
|
733
|
-
scope: 'scope1',
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
credentials.set({
|
|
737
|
-
supertoken: st,
|
|
738
|
-
userTokens: [t1],
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
return credentials
|
|
742
|
-
.invalidate()
|
|
743
|
-
.then(() =>
|
|
744
|
-
assert.isRejected(
|
|
745
|
-
credentials.getUserToken(),
|
|
746
|
-
/Current state cannot produce an access token/
|
|
747
|
-
)
|
|
748
|
-
);
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
describe('#refresh()', () => {
|
|
753
|
-
it('refreshes and downscopes the supertoken, and revokes previous tokens', () => {
|
|
754
|
-
const webex = new MockWebex();
|
|
755
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
756
|
-
|
|
757
|
-
webex.trigger('change:config');
|
|
758
|
-
const st = makeToken(webex, {
|
|
759
|
-
access_token: 'ST',
|
|
760
|
-
refresh_token: 'RT',
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const st2 = makeToken(webex, {
|
|
764
|
-
access_token: 'ST2',
|
|
765
|
-
refresh_token: 'RT2',
|
|
766
|
-
});
|
|
767
|
-
|
|
768
|
-
const t1 = makeToken(webex, {
|
|
769
|
-
access_token: 'AT1',
|
|
770
|
-
scope: 'scope1',
|
|
771
|
-
});
|
|
772
|
-
|
|
773
|
-
const t2 = makeToken(webex, {
|
|
774
|
-
access_token: 'AT2',
|
|
775
|
-
scope: 'scope2',
|
|
776
|
-
});
|
|
777
|
-
|
|
778
|
-
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
779
|
-
sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
|
|
780
|
-
sinon.stub(t1, 'revoke').returns(Promise.resolve());
|
|
781
|
-
sinon.spy(credentials, 'scheduleRefresh');
|
|
782
|
-
|
|
783
|
-
credentials.set({
|
|
784
|
-
supertoken: st,
|
|
785
|
-
userTokens: [t1],
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
assert.equal(credentials.userTokens.get(t1.scope), t1);
|
|
789
|
-
|
|
790
|
-
return credentials
|
|
791
|
-
.refresh()
|
|
792
|
-
.then(() => assert.called(st.refresh))
|
|
793
|
-
.then(() => assert.calledWith(st2.downscope, 'scope1'))
|
|
794
|
-
.then(() => assert.called(t1.revoke))
|
|
795
|
-
.then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
|
|
796
|
-
.then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
|
|
797
|
-
.then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
|
|
798
|
-
});
|
|
799
|
-
|
|
800
|
-
it('refreshes and downscopes the supertoken even if revocation of previous token fails', () => {
|
|
801
|
-
const webex = new MockWebex();
|
|
802
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
803
|
-
|
|
804
|
-
webex.trigger('change:config');
|
|
805
|
-
const st = makeToken(webex, {
|
|
806
|
-
access_token: 'ST',
|
|
807
|
-
refresh_token: 'RT',
|
|
808
|
-
});
|
|
809
|
-
|
|
810
|
-
const st2 = makeToken(webex, {
|
|
811
|
-
access_token: 'ST2',
|
|
812
|
-
refresh_token: 'RT2',
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
const t1 = makeToken(webex, {
|
|
816
|
-
access_token: 'AT1',
|
|
817
|
-
scope: 'scope1',
|
|
818
|
-
});
|
|
819
|
-
|
|
820
|
-
const t2 = makeToken(webex, {
|
|
821
|
-
access_token: 'AT2',
|
|
822
|
-
scope: 'scope2',
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
826
|
-
sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
|
|
827
|
-
sinon.stub(t1, 'revoke').returns(Promise.reject());
|
|
828
|
-
sinon.spy(credentials, 'scheduleRefresh');
|
|
829
|
-
|
|
830
|
-
credentials.set({
|
|
831
|
-
supertoken: st,
|
|
832
|
-
userTokens: [t1],
|
|
833
|
-
});
|
|
834
|
-
|
|
835
|
-
assert.equal(credentials.userTokens.get(t1.scope), t1);
|
|
836
|
-
|
|
837
|
-
return credentials
|
|
838
|
-
.refresh()
|
|
839
|
-
.then(() => assert.called(st.refresh))
|
|
840
|
-
.then(() => assert.calledWith(st2.downscope, 'scope1'))
|
|
841
|
-
.then(() => assert.called(t1.revoke))
|
|
842
|
-
.then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
|
|
843
|
-
.then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
|
|
844
|
-
.then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
|
|
845
|
-
});
|
|
846
|
-
|
|
847
|
-
it('removes and revokes all child tokens', () => {
|
|
848
|
-
const webex = new MockWebex({
|
|
849
|
-
children: {
|
|
850
|
-
logger: Logger,
|
|
851
|
-
},
|
|
852
|
-
});
|
|
853
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
854
|
-
|
|
855
|
-
webex.trigger('change:config');
|
|
856
|
-
const st = makeToken(webex, {
|
|
857
|
-
access_token: 'ST',
|
|
858
|
-
refresh_token: 'RT',
|
|
859
|
-
});
|
|
860
|
-
|
|
861
|
-
sinon.stub(st, 'refresh').returns(Promise.resolve(makeToken(webex, {access_token: 'ST2'})));
|
|
862
|
-
|
|
863
|
-
const t1 = makeToken(webex, {
|
|
864
|
-
access_token: 'AT1',
|
|
865
|
-
scope: 'scope1',
|
|
866
|
-
});
|
|
867
|
-
|
|
868
|
-
credentials.set({
|
|
869
|
-
supertoken: st,
|
|
870
|
-
userTokens: [t1],
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
return credentials.refresh().then(() => assert.called(st.refresh));
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
it('allows #getUserToken() to be revoked, but #getUserToken() promises will not resolve until the suport token has been refreshed', () => {
|
|
877
|
-
const webex = new MockWebex();
|
|
878
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
879
|
-
|
|
880
|
-
webex.trigger('change:config');
|
|
881
|
-
const st1 = makeToken(webex, {
|
|
882
|
-
access_token: 'ST1',
|
|
883
|
-
refresh_token: 'RT1',
|
|
884
|
-
});
|
|
885
|
-
|
|
886
|
-
const st2 = makeToken(webex, {
|
|
887
|
-
access_token: 'ST2',
|
|
888
|
-
refresh_token: 'RT1',
|
|
889
|
-
});
|
|
890
|
-
|
|
891
|
-
const t1 = makeToken(webex, {
|
|
892
|
-
access_token: 'AT1',
|
|
893
|
-
scope: 'scope1',
|
|
894
|
-
});
|
|
895
|
-
|
|
896
|
-
const t2 = makeToken(webex, {
|
|
897
|
-
access_token: 'AT2',
|
|
898
|
-
scope: 'scope1',
|
|
899
|
-
});
|
|
900
|
-
|
|
901
|
-
sinon.stub(st1, 'refresh').returns(Promise.resolve(st2));
|
|
902
|
-
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
903
|
-
|
|
904
|
-
credentials.set({
|
|
905
|
-
supertoken: st1,
|
|
906
|
-
userTokens: [t1],
|
|
907
|
-
});
|
|
908
|
-
|
|
909
|
-
credentials.refresh();
|
|
910
|
-
|
|
911
|
-
return credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t2));
|
|
912
|
-
});
|
|
913
|
-
|
|
914
|
-
it('emits InvalidRequestError when the refresh token and access token expire', () => {
|
|
915
|
-
const webex = new MockWebex();
|
|
916
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
917
|
-
|
|
918
|
-
webex.trigger('change:config');
|
|
919
|
-
const st = makeToken(webex, {
|
|
920
|
-
access_token: 'ST',
|
|
921
|
-
refresh_token: 'RT',
|
|
922
|
-
});
|
|
923
|
-
|
|
924
|
-
const t1 = makeToken(webex, {
|
|
925
|
-
access_token: 'AT1',
|
|
926
|
-
scope: 'scope1',
|
|
927
|
-
});
|
|
928
|
-
|
|
929
|
-
const res = {
|
|
930
|
-
body: {
|
|
931
|
-
error: 'invalid_request',
|
|
932
|
-
error_description:
|
|
933
|
-
'The refresh token provided is expired, revoked, malformed, or invalid.',
|
|
934
|
-
trackingID: 'test123',
|
|
935
|
-
},
|
|
936
|
-
};
|
|
937
|
-
|
|
938
|
-
const ErrorConstructor = grantErrors.select(res.body.error);
|
|
939
|
-
|
|
940
|
-
sinon
|
|
941
|
-
.stub(st, 'refresh')
|
|
942
|
-
.returns(Promise.reject(new ErrorConstructor('InvalidRequestError')));
|
|
943
|
-
sinon.stub(credentials, 'unset').returns(Promise.resolve());
|
|
944
|
-
const triggerSpy = sinon.spy(webex, 'trigger');
|
|
945
|
-
|
|
946
|
-
credentials.set({
|
|
947
|
-
supertoken: st,
|
|
948
|
-
userTokens: [t1],
|
|
949
|
-
});
|
|
950
|
-
|
|
951
|
-
return credentials
|
|
952
|
-
.refresh()
|
|
953
|
-
.then(() => assert.called(st.refresh))
|
|
954
|
-
.catch(() => {
|
|
955
|
-
assert.called(credentials.unset);
|
|
956
|
-
assert.calledWith(triggerSpy, sinon.match('client:InvalidRequestError'));
|
|
957
|
-
});
|
|
958
|
-
});
|
|
959
|
-
});
|
|
960
|
-
|
|
961
|
-
describe('#scheduleRefresh()', () => {
|
|
962
|
-
it('refreshes token immediately if token is expired', () => {
|
|
963
|
-
const webex = new MockWebex();
|
|
964
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
965
|
-
|
|
966
|
-
webex.trigger('change:config');
|
|
967
|
-
const st = makeToken(webex, {
|
|
968
|
-
access_token: 'ST',
|
|
969
|
-
refresh_token: 'RT',
|
|
970
|
-
});
|
|
971
|
-
|
|
972
|
-
const st2 = makeToken(webex, {
|
|
973
|
-
access_token: 'ST2',
|
|
974
|
-
refresh_token: 'RT2',
|
|
975
|
-
});
|
|
976
|
-
|
|
977
|
-
credentials.set({
|
|
978
|
-
supertoken: st,
|
|
979
|
-
});
|
|
980
|
-
|
|
981
|
-
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
982
|
-
|
|
983
|
-
credentials.scheduleRefresh(Date.now() - 10000);
|
|
984
|
-
assert.isUndefined(credentials.refreshTimer);
|
|
985
|
-
assert.called(credentials.refresh);
|
|
986
|
-
});
|
|
987
|
-
|
|
988
|
-
it('schedules a token refresh', () => {
|
|
989
|
-
const webex = new MockWebex();
|
|
990
|
-
const credentials = new Credentials(undefined, {parent: webex});
|
|
991
|
-
|
|
992
|
-
webex.trigger('change:config');
|
|
993
|
-
const st = makeToken(webex, {
|
|
994
|
-
access_token: 'ST',
|
|
995
|
-
refresh_token: 'RT',
|
|
996
|
-
});
|
|
997
|
-
|
|
998
|
-
const st2 = makeToken(webex, {
|
|
999
|
-
access_token: 'ST2',
|
|
1000
|
-
refresh_token: 'RT2',
|
|
1001
|
-
});
|
|
1002
|
-
|
|
1003
|
-
credentials.set({
|
|
1004
|
-
supertoken: st,
|
|
1005
|
-
});
|
|
1006
|
-
|
|
1007
|
-
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
1008
|
-
|
|
1009
|
-
credentials.scheduleRefresh(Date.now() + 10000);
|
|
1010
|
-
assert.isDefined(credentials.refreshTimer);
|
|
1011
|
-
assert.notCalled(credentials.refresh);
|
|
1012
|
-
clock.tick(10000);
|
|
1013
|
-
assert.called(credentials.refresh);
|
|
1014
|
-
});
|
|
1015
|
-
});
|
|
1016
|
-
});
|
|
1017
|
-
});
|
|
1
|
+
/*!
|
|
2
|
+
* Copyright (c) 2015-2020 Cisco Systems, Inc. See LICENSE file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {set} from 'lodash';
|
|
6
|
+
import {assert} from '@webex/test-helper-chai';
|
|
7
|
+
import sinon from 'sinon';
|
|
8
|
+
import MockWebex from '@webex/test-helper-mock-webex';
|
|
9
|
+
import {Credentials, Token, grantErrors} from '@webex/webex-core';
|
|
10
|
+
import {inBrowser} from '@webex/common';
|
|
11
|
+
import FakeTimers from '@sinonjs/fake-timers';
|
|
12
|
+
import {skipInBrowser} from '@webex/test-helper-mocha';
|
|
13
|
+
import Logger from '@webex/plugin-logger';
|
|
14
|
+
|
|
15
|
+
/* eslint camelcase: [0] */
|
|
16
|
+
|
|
17
|
+
// eslint-disable-next-line no-empty-function
|
|
18
|
+
function noop() {}
|
|
19
|
+
|
|
20
|
+
function promiseTick(count) {
|
|
21
|
+
let promise = Promise.resolve();
|
|
22
|
+
|
|
23
|
+
while (count > 1) {
|
|
24
|
+
promise = promise.then(() => promiseTick(1));
|
|
25
|
+
count -= 1;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return promise;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const AUTHORIZATION_STRING =
|
|
32
|
+
'https://api.ciscospark.com/v1/authorize?client_id=MOCK_CLIENT_ID&response_type=code&redirect_uri=http%3A%2F%2Flocalhost%3A8000&scope=spark%3Arooms_read%20spark%3Ateams_read&state=set_state_here';
|
|
33
|
+
|
|
34
|
+
describe('webex-core', () => {
|
|
35
|
+
describe('Credentials', () => {
|
|
36
|
+
let clock;
|
|
37
|
+
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
clock = FakeTimers.install({now: Date.now()});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
clock.uninstall();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function makeToken(webex, options) {
|
|
47
|
+
return new Token(options, {parent: webex});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('#calcRefreshTimeout', () => {
|
|
51
|
+
it('generates a number between 60-90% of expiration', () => {
|
|
52
|
+
const expiration = 1000;
|
|
53
|
+
const webex = new MockWebex();
|
|
54
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
55
|
+
const result = credentials.calcRefreshTimeout(expiration);
|
|
56
|
+
|
|
57
|
+
assert.isTrue(result >= expiration * 0.6);
|
|
58
|
+
assert.isTrue(result <= expiration * 0.9);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
describe('#canAuthorize', () => {
|
|
63
|
+
it('indicates if the current state has enough information to populate an auth header, even if a token refresh or token downscope is required', () => {
|
|
64
|
+
const webex = new MockWebex();
|
|
65
|
+
|
|
66
|
+
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
67
|
+
let credentials = new Credentials(undefined, {parent: webex});
|
|
68
|
+
|
|
69
|
+
webex.trigger('change:config');
|
|
70
|
+
assert.isFalse(credentials.canAuthorize);
|
|
71
|
+
|
|
72
|
+
credentials.supertoken = makeToken(webex, {
|
|
73
|
+
access_token: 'AT',
|
|
74
|
+
});
|
|
75
|
+
assert.isTrue(credentials.canAuthorize);
|
|
76
|
+
|
|
77
|
+
credentials.supertoken.unset('access_token');
|
|
78
|
+
assert.isFalse(credentials.canAuthorize);
|
|
79
|
+
|
|
80
|
+
credentials.supertoken = makeToken(webex, {
|
|
81
|
+
access_token: 'AT',
|
|
82
|
+
});
|
|
83
|
+
assert.isTrue(credentials.canAuthorize);
|
|
84
|
+
|
|
85
|
+
credentials.supertoken = makeToken(webex, {
|
|
86
|
+
access_token: 'AT',
|
|
87
|
+
expires: Date.now() - 10000,
|
|
88
|
+
});
|
|
89
|
+
assert.isFalse(credentials.supertoken.canAuthorize);
|
|
90
|
+
assert.isFalse(credentials.canRefresh);
|
|
91
|
+
assert.isFalse(credentials.canAuthorize);
|
|
92
|
+
|
|
93
|
+
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
94
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
95
|
+
webex.trigger('change:config');
|
|
96
|
+
assert.isFalse(credentials.canAuthorize);
|
|
97
|
+
credentials.supertoken = makeToken(webex, {
|
|
98
|
+
access_token: 'AT',
|
|
99
|
+
refresh_token: 'RT',
|
|
100
|
+
});
|
|
101
|
+
credentials.supertoken.unset('access_token');
|
|
102
|
+
assert.isTrue(credentials.canAuthorize);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('#canRefresh', () => {
|
|
107
|
+
it('indicates if there is presently enough information to refresh', () => {
|
|
108
|
+
const webex = new MockWebex();
|
|
109
|
+
let credentials = new Credentials(undefined, {parent: webex});
|
|
110
|
+
|
|
111
|
+
webex.trigger('change:config');
|
|
112
|
+
assert.isFalse(credentials.canRefresh);
|
|
113
|
+
credentials.supertoken = makeToken(
|
|
114
|
+
webex,
|
|
115
|
+
{
|
|
116
|
+
access_token: 'AT',
|
|
117
|
+
},
|
|
118
|
+
{parent: true}
|
|
119
|
+
);
|
|
120
|
+
assert.isFalse(credentials.canRefresh);
|
|
121
|
+
|
|
122
|
+
webex.config.credentials.refreshCallback = inBrowser && noop;
|
|
123
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
124
|
+
webex.trigger('change:config');
|
|
125
|
+
assert.isFalse(credentials.canRefresh);
|
|
126
|
+
credentials.supertoken = makeToken(webex, {
|
|
127
|
+
access_token: 'AT',
|
|
128
|
+
refresh_token: 'RT',
|
|
129
|
+
});
|
|
130
|
+
assert.isTrue(credentials.supertoken.canRefresh);
|
|
131
|
+
assert.isTrue(credentials.canRefresh);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('#buildLoginUrl()', () => {
|
|
136
|
+
it('requires `state` to be an object', () => {
|
|
137
|
+
const webex = new MockWebex({
|
|
138
|
+
children: {
|
|
139
|
+
credentials: Credentials,
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
webex.trigger('change:config)');
|
|
144
|
+
assert.doesNotThrow(() => {
|
|
145
|
+
webex.credentials.buildLoginUrl();
|
|
146
|
+
}, /if specified, `options.state` must be an object/);
|
|
147
|
+
|
|
148
|
+
assert.doesNotThrow(() => {
|
|
149
|
+
webex.credentials.buildLoginUrl({});
|
|
150
|
+
}, /if specified, `options.state` must be an object/);
|
|
151
|
+
|
|
152
|
+
assert.throws(() => {
|
|
153
|
+
webex.credentials.buildLoginUrl({state: 'state'});
|
|
154
|
+
}, /if specified, `options.state` must be an object/);
|
|
155
|
+
|
|
156
|
+
assert.doesNotThrow(() => {
|
|
157
|
+
webex.credentials.buildLoginUrl({state: {}});
|
|
158
|
+
}, /if specified, `options.state` must be an object/);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('prefers the hydra auth url, but falls back to idbroker', () => {
|
|
162
|
+
const webex = new MockWebex();
|
|
163
|
+
let credentials = new Credentials(undefined, {parent: webex});
|
|
164
|
+
|
|
165
|
+
webex.trigger('change:config');
|
|
166
|
+
|
|
167
|
+
assert.match(credentials.buildLoginUrl({state: {}}), /idbroker/);
|
|
168
|
+
webex.config.credentials = {
|
|
169
|
+
authorizationString: AUTHORIZATION_STRING,
|
|
170
|
+
};
|
|
171
|
+
credentials = new Credentials({}, {parent: webex});
|
|
172
|
+
webex.trigger('change:config');
|
|
173
|
+
assert.match(credentials.buildLoginUrl({state: {}}), /api.ciscospark.com/);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
skipInBrowser(it)('generates the login url', () => {
|
|
177
|
+
const webex = new MockWebex();
|
|
178
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
179
|
+
|
|
180
|
+
webex.trigger('change:config');
|
|
181
|
+
|
|
182
|
+
assert.equal(
|
|
183
|
+
credentials.buildLoginUrl({state: {page: 'front'}}),
|
|
184
|
+
`${
|
|
185
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
186
|
+
}/idb/oauth2/v1/authorize?state=eyJwYWdlIjoiZnJvbnQifQ&client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
skipInBrowser(it)('generates the login url with empty state param', () => {
|
|
191
|
+
const webex = new MockWebex();
|
|
192
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
193
|
+
|
|
194
|
+
webex.trigger('change:config');
|
|
195
|
+
|
|
196
|
+
assert.equal(
|
|
197
|
+
credentials.buildLoginUrl({state: {}}),
|
|
198
|
+
`${
|
|
199
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
200
|
+
}/idb/oauth2/v1/authorize?client_id=fake&redirect_uri=http%3A%2F%2Fexample.com&scope=scope%3Aone&response_type=code`
|
|
201
|
+
);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
describe('#buildLogoutUrl()', () => {
|
|
206
|
+
skipInBrowser(it)('generates the logout url', () => {
|
|
207
|
+
const webex = new MockWebex();
|
|
208
|
+
|
|
209
|
+
webex.config.credentials.redirect_uri = 'ru';
|
|
210
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
211
|
+
|
|
212
|
+
webex.trigger('change:config');
|
|
213
|
+
assert.equal(
|
|
214
|
+
credentials.buildLogoutUrl(),
|
|
215
|
+
`${
|
|
216
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
217
|
+
}/idb/oauth2/v1/logout?cisService=webex&goto=ru`
|
|
218
|
+
);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
skipInBrowser(it)('includes a token param if passed', () => {
|
|
222
|
+
const webex = new MockWebex();
|
|
223
|
+
|
|
224
|
+
webex.config.credentials.redirect_uri = 'ru';
|
|
225
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
226
|
+
|
|
227
|
+
webex.trigger('change:config');
|
|
228
|
+
assert.equal(
|
|
229
|
+
credentials.buildLogoutUrl({token: 't'}),
|
|
230
|
+
`${
|
|
231
|
+
process.env.IDBROKER_BASE_URL || 'https://idbroker.webex.com'
|
|
232
|
+
}/idb/oauth2/v1/logout?cisService=webex&goto=ru&token=t`
|
|
233
|
+
);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('always fallsback to idbroker', () => {
|
|
237
|
+
const webex = new MockWebex();
|
|
238
|
+
|
|
239
|
+
webex.config.credentials.redirect_uri = 'ru';
|
|
240
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
241
|
+
|
|
242
|
+
webex.trigger('change:config');
|
|
243
|
+
assert.match(credentials.buildLogoutUrl(), /idbroker.*?goto=ru/);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
it('allows overriding the goto url', () => {
|
|
247
|
+
const webex = new MockWebex();
|
|
248
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
249
|
+
|
|
250
|
+
webex.trigger('change:config');
|
|
251
|
+
assert.match(
|
|
252
|
+
credentials.buildLogoutUrl({goto: 'http://example.com/'}),
|
|
253
|
+
/goto=http%3A%2F%2Fexample.com%2F/
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
describe('#getOrgId()', () => {
|
|
259
|
+
let credentials;
|
|
260
|
+
let orgId;
|
|
261
|
+
let webex;
|
|
262
|
+
|
|
263
|
+
beforeEach(() => {
|
|
264
|
+
webex = new MockWebex();
|
|
265
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
it('should return the OrgId of JWT-authenticated user', () => {
|
|
269
|
+
credentials.supertoken = {
|
|
270
|
+
access_token:
|
|
271
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk',
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
orgId = 'my-realm';
|
|
275
|
+
|
|
276
|
+
assert.equal(credentials.getOrgId(), orgId);
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should return the OrgId of a user-token-authenticated user', () => {
|
|
280
|
+
orgId = 'this-is-an-org-id';
|
|
281
|
+
|
|
282
|
+
credentials.supertoken = {
|
|
283
|
+
access_token: `000_000_${orgId}`,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
assert.equal(credentials.getOrgId(), orgId);
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should throw if the OrgId was not determined', () =>
|
|
290
|
+
expect(() => credentials.getOrgId()).toThrow('the provided token is not a valid format'));
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
describe('#extractOrgIdFromJWT()', () => {
|
|
294
|
+
let credentials;
|
|
295
|
+
let webex;
|
|
296
|
+
|
|
297
|
+
beforeEach(() => {
|
|
298
|
+
webex = new MockWebex();
|
|
299
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should return the OrgId of a provided JWT', () => {
|
|
303
|
+
const jwt =
|
|
304
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJyZWFsbSI6Im15LXJlYWxtIn0.U16gzUsaRW1VVikJA2VeXRHPX716tG1_B42oxzy1UMk';
|
|
305
|
+
const realm = 'my-realm';
|
|
306
|
+
|
|
307
|
+
assert.equal(credentials.extractOrgIdFromJWT(jwt), realm);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should throw if the provided JWT is not valid', () =>
|
|
311
|
+
expect(() => credentials.extractOrgIdFromJWT('not-valid')).toThrow());
|
|
312
|
+
|
|
313
|
+
it('should throw if the provided JWT does not contain an OrgId', () => {
|
|
314
|
+
const jwtNoOrg =
|
|
315
|
+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
|
|
316
|
+
|
|
317
|
+
expect(() => credentials.extractOrgIdFromJWT(jwtNoOrg)).toThrow();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
it('should throw if no JWT was provided', () =>
|
|
321
|
+
expect(() => credentials.extractOrgIdFromJWT()).toThrow());
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
describe('#extractOrgIdFromUserToken()', () => {
|
|
325
|
+
let credentials;
|
|
326
|
+
let webex;
|
|
327
|
+
|
|
328
|
+
beforeEach(() => {
|
|
329
|
+
webex = new MockWebex();
|
|
330
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should return the OrgId of the provided user token', () => {
|
|
334
|
+
const orgId = 'this-is-an-org-id';
|
|
335
|
+
const userToken = `000_000_${orgId}`;
|
|
336
|
+
|
|
337
|
+
assert.equal(credentials.extractOrgIdFromUserToken(userToken), orgId);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('should throw when provided an invalid token', () =>
|
|
341
|
+
expect(() => credentials.extractOrgIdFromUserToken()).toThrow('the provided token is not a valid format'));
|
|
342
|
+
|
|
343
|
+
it('should throw when no token is provided', () =>
|
|
344
|
+
expect(() => credentials.extractOrgIdFromUserToken()).toThrow());
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
describe('#initialize()', () => {
|
|
348
|
+
it('turns a portal auth string into a configuration object', () => {
|
|
349
|
+
const webex = new MockWebex();
|
|
350
|
+
|
|
351
|
+
webex.config.credentials = {
|
|
352
|
+
client_id: 'ci',
|
|
353
|
+
redirect_uri: 'ru',
|
|
354
|
+
scope: 's',
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
let credentials = new Credentials(undefined, {parent: webex});
|
|
358
|
+
|
|
359
|
+
webex.trigger('change:config');
|
|
360
|
+
webex.trigger('change:config');
|
|
361
|
+
assert.equal(webex.config.credentials.client_id, 'ci');
|
|
362
|
+
assert.equal(credentials.config.client_id, 'ci');
|
|
363
|
+
assert.equal(webex.config.credentials.redirect_uri, 'ru');
|
|
364
|
+
assert.equal(credentials.config.redirect_uri, 'ru');
|
|
365
|
+
assert.equal(webex.config.credentials.scope, 's');
|
|
366
|
+
assert.equal(credentials.config.scope, 's');
|
|
367
|
+
|
|
368
|
+
// Accept a portal auth string via environment variables
|
|
369
|
+
webex.config.credentials.authorizationString = AUTHORIZATION_STRING;
|
|
370
|
+
credentials = new Credentials(undefined, {parent: webex});
|
|
371
|
+
webex.trigger('change:config');
|
|
372
|
+
webex.trigger('change:config');
|
|
373
|
+
assert.equal(webex.config.credentials.client_id, 'MOCK_CLIENT_ID');
|
|
374
|
+
assert.equal(credentials.config.client_id, 'MOCK_CLIENT_ID');
|
|
375
|
+
assert.equal(webex.config.credentials.redirect_uri, 'http://localhost:8000');
|
|
376
|
+
assert.equal(credentials.config.redirect_uri, 'http://localhost:8000');
|
|
377
|
+
assert.equal(webex.config.credentials.scope, 'spark:rooms_read spark:teams_read');
|
|
378
|
+
assert.equal(credentials.config.scope, 'spark:rooms_read spark:teams_read');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
[
|
|
382
|
+
'data',
|
|
383
|
+
'data.access_token',
|
|
384
|
+
'data.supertoken',
|
|
385
|
+
'data.supertoken.access_token',
|
|
386
|
+
'data.authorization',
|
|
387
|
+
'data.authorization.supertoken',
|
|
388
|
+
'data.authorization.supertoken.access_token',
|
|
389
|
+
]
|
|
390
|
+
.reduce(
|
|
391
|
+
(acc, path) =>
|
|
392
|
+
acc.concat(
|
|
393
|
+
['ST', 'Bearer ST'].map((str) => {
|
|
394
|
+
const obj = {
|
|
395
|
+
msg: `accepts token string "${str}" at path "${path
|
|
396
|
+
.split('.')
|
|
397
|
+
.slice(1)
|
|
398
|
+
.join('.')}"`,
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
set(obj, path, str);
|
|
402
|
+
|
|
403
|
+
return obj;
|
|
404
|
+
})
|
|
405
|
+
),
|
|
406
|
+
[]
|
|
407
|
+
)
|
|
408
|
+
.forEach(({msg, data}) => {
|
|
409
|
+
it(msg, () => {
|
|
410
|
+
const webex = new MockWebex();
|
|
411
|
+
const credentials = new Credentials(data, {parent: webex});
|
|
412
|
+
|
|
413
|
+
assert.isTrue(credentials.canAuthorize);
|
|
414
|
+
assert.equal(credentials.supertoken.access_token, 'ST');
|
|
415
|
+
assert.equal(credentials.supertoken.token_type, 'Bearer');
|
|
416
|
+
});
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('schedules a refreshTimer', () => {
|
|
420
|
+
const webex = new MockWebex();
|
|
421
|
+
const supertoken = makeToken(webex, {
|
|
422
|
+
access_token: 'ST',
|
|
423
|
+
refresh_token: 'RT',
|
|
424
|
+
expires: Date.now() + 10000,
|
|
425
|
+
});
|
|
426
|
+
const supertoken2 = makeToken(webex, {
|
|
427
|
+
access_token: 'ST2',
|
|
428
|
+
refresh_token: 'RT2',
|
|
429
|
+
expires: Date.now() + 20000,
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
sinon.stub(supertoken, 'refresh').returns(Promise.resolve(supertoken2));
|
|
433
|
+
const credentials = new Credentials(supertoken, {parent: webex});
|
|
434
|
+
|
|
435
|
+
webex.trigger('change:config');
|
|
436
|
+
|
|
437
|
+
const firstTimer = credentials.refreshTimer;
|
|
438
|
+
|
|
439
|
+
assert.isDefined(firstTimer);
|
|
440
|
+
assert.notCalled(supertoken.refresh);
|
|
441
|
+
clock.tick(10000);
|
|
442
|
+
|
|
443
|
+
return promiseTick(8)
|
|
444
|
+
.then(() => assert.called(supertoken.refresh))
|
|
445
|
+
.then(() => assert.isDefined(credentials.refreshTimer))
|
|
446
|
+
.then(() => assert.notEqual(credentials.refreshTimer, firstTimer));
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it.skip('does not schedule a refreshTimer', () => {
|
|
450
|
+
const webex = new MockWebex();
|
|
451
|
+
const supertoken = makeToken(webex, {
|
|
452
|
+
access_token: 'ST',
|
|
453
|
+
refresh_token: 'RT',
|
|
454
|
+
expires: Date.now() - 10000,
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
sinon.stub(supertoken, 'refresh').returns(Promise.reject());
|
|
458
|
+
const credentials = new Credentials(supertoken, {parent: webex});
|
|
459
|
+
|
|
460
|
+
webex.trigger('change:config');
|
|
461
|
+
|
|
462
|
+
assert.isUndefined(credentials.refreshTimer);
|
|
463
|
+
});
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
describe('#getUserToken()', () => {
|
|
467
|
+
// it('resolves with the supertoken if the supertoken matches the requested scopes');
|
|
468
|
+
|
|
469
|
+
it('resolves with the token identified by the specified scopes', () => {
|
|
470
|
+
const webex = new MockWebex();
|
|
471
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
472
|
+
|
|
473
|
+
webex.trigger('change:config');
|
|
474
|
+
const st = makeToken(webex, {access_token: 'ST'});
|
|
475
|
+
const t1 = makeToken(webex, {
|
|
476
|
+
access_token: 'AT1',
|
|
477
|
+
scope: 'scope1',
|
|
478
|
+
});
|
|
479
|
+
const t2 = makeToken(webex, {
|
|
480
|
+
access_token: 'AT2',
|
|
481
|
+
scope: 'scope2',
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
credentials.set({
|
|
485
|
+
supertoken: st,
|
|
486
|
+
userTokens: [t1, t2],
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
return Promise.all([
|
|
490
|
+
credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t1)),
|
|
491
|
+
credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, t2)),
|
|
492
|
+
]);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
describe('when no matching token is found', () => {
|
|
496
|
+
it('downscopes the supertoken', () => {
|
|
497
|
+
const webex = new MockWebex();
|
|
498
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
499
|
+
|
|
500
|
+
webex.trigger('change:config');
|
|
501
|
+
|
|
502
|
+
credentials.supertoken = makeToken(webex, {
|
|
503
|
+
access_token: 'ST',
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
const t2 = makeToken(webex, {
|
|
507
|
+
access_token: 'AT2',
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
sinon.stub(credentials.supertoken, 'downscope').returns(Promise.resolve(t2));
|
|
511
|
+
|
|
512
|
+
const t1 = makeToken(webex, {
|
|
513
|
+
access_token: 'AT1',
|
|
514
|
+
scope: 'scope1',
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
credentials.set({
|
|
518
|
+
userTokens: [t1],
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return credentials
|
|
522
|
+
.getUserToken('scope2')
|
|
523
|
+
.then((result) => assert.deepEqual(result, t2))
|
|
524
|
+
.then(() => assert.calledWith(credentials.supertoken.downscope, 'scope2'));
|
|
525
|
+
});
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
describe('when no scope is specified', () => {
|
|
529
|
+
it('resolves with a token containing all but the kms scopes', () => {
|
|
530
|
+
const webex = new MockWebex();
|
|
531
|
+
|
|
532
|
+
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
533
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
534
|
+
|
|
535
|
+
webex.trigger('change:config');
|
|
536
|
+
|
|
537
|
+
credentials.supertoken = makeToken(webex, {
|
|
538
|
+
access_token: 'ST',
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
// const t2 = makeToken(webex, {
|
|
542
|
+
// access_token: `AT2`
|
|
543
|
+
// });
|
|
544
|
+
|
|
545
|
+
// sinon.stub(credentials.supertoken, `downscope`).returns(Promise.resolve(t2));
|
|
546
|
+
|
|
547
|
+
const t1 = makeToken(webex, {
|
|
548
|
+
access_token: 'AT1',
|
|
549
|
+
scope: 'scope1',
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
credentials.set({
|
|
553
|
+
userTokens: [t1],
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
return credentials.getUserToken().then((result) => assert.deepEqual(result, t1));
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
describe('when the kms downscope request fails', () => {
|
|
561
|
+
it('falls back to the supertoken', () => {
|
|
562
|
+
const webex = new MockWebex({
|
|
563
|
+
children: {
|
|
564
|
+
logger: Logger,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
569
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
570
|
+
|
|
571
|
+
webex.trigger('change:config');
|
|
572
|
+
|
|
573
|
+
credentials.supertoken = makeToken(webex, {
|
|
574
|
+
access_token: 'ST',
|
|
575
|
+
});
|
|
576
|
+
|
|
577
|
+
sinon
|
|
578
|
+
.stub(credentials.supertoken, 'downscope')
|
|
579
|
+
.returns(Promise.reject(new Error('downscope failed')));
|
|
580
|
+
|
|
581
|
+
const t1 = makeToken(webex, {
|
|
582
|
+
access_token: 'AT1',
|
|
583
|
+
scope: 'scope1',
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
credentials.set({
|
|
587
|
+
userTokens: [t1],
|
|
588
|
+
});
|
|
589
|
+
|
|
590
|
+
return credentials
|
|
591
|
+
.getUserToken('scope2')
|
|
592
|
+
.then((t) => assert.equal(t.access_token, credentials.supertoken.access_token));
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
it('is blocked while a token refresh is inflight', () => {
|
|
597
|
+
const webex = new MockWebex();
|
|
598
|
+
|
|
599
|
+
webex.config.credentials.scope = 'scope1 spark:kms';
|
|
600
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
601
|
+
|
|
602
|
+
webex.trigger('change:config');
|
|
603
|
+
|
|
604
|
+
const supertoken1 = makeToken(webex, {
|
|
605
|
+
access_token: 'ST1',
|
|
606
|
+
refresh_token: 'RT1',
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
credentials.set({supertoken: supertoken1});
|
|
610
|
+
|
|
611
|
+
sinon
|
|
612
|
+
.stub(supertoken1, 'downscope')
|
|
613
|
+
.returns(Promise.resolve(new Token({access_token: 'ST1ATD'})));
|
|
614
|
+
const supertoken2 = makeToken(webex, {
|
|
615
|
+
access_token: 'ST2',
|
|
616
|
+
});
|
|
617
|
+
|
|
618
|
+
sinon.stub(supertoken1, 'refresh').returns(Promise.resolve(supertoken2));
|
|
619
|
+
|
|
620
|
+
const at2 = makeToken(webex, {access_token: 'ST2ATD'});
|
|
621
|
+
|
|
622
|
+
sinon.stub(supertoken2, 'downscope').returns(Promise.resolve(at2));
|
|
623
|
+
|
|
624
|
+
return Promise.all([
|
|
625
|
+
credentials.refresh(),
|
|
626
|
+
credentials.getUserToken('scope2').then((result) => assert.deepEqual(result, at2)),
|
|
627
|
+
]);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
|
|
631
|
+
describe('#invalidate()', () => {
|
|
632
|
+
it('clears the refreshTimer', () => {
|
|
633
|
+
const webex = new MockWebex();
|
|
634
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
635
|
+
|
|
636
|
+
webex.trigger('change:config');
|
|
637
|
+
const st = makeToken(webex, {
|
|
638
|
+
access_token: 'ST',
|
|
639
|
+
refresh_token: 'RT',
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
const st2 = makeToken(webex, {
|
|
643
|
+
access_token: 'ST2',
|
|
644
|
+
refresh_token: 'RT2',
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
credentials.set({
|
|
648
|
+
supertoken: st,
|
|
649
|
+
});
|
|
650
|
+
|
|
651
|
+
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
652
|
+
|
|
653
|
+
credentials.scheduleRefresh(Date.now() + 10000);
|
|
654
|
+
assert.isDefined(credentials.refreshTimer);
|
|
655
|
+
assert.notCalled(credentials.refresh);
|
|
656
|
+
|
|
657
|
+
return credentials.invalidate().then(() => {
|
|
658
|
+
clock.tick(10000);
|
|
659
|
+
assert.isUndefined(credentials.refreshTimer);
|
|
660
|
+
assert.notCalled(credentials.refresh);
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('clears the tokens from boundedStorage', () => {
|
|
665
|
+
const webex = new MockWebex();
|
|
666
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
667
|
+
|
|
668
|
+
webex.trigger('change:config');
|
|
669
|
+
const st = makeToken(webex, {
|
|
670
|
+
access_token: 'ST',
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
const t1 = makeToken(webex, {
|
|
674
|
+
access_token: 'AT1',
|
|
675
|
+
scope: 'scope1',
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
const t2 = makeToken(webex, {
|
|
679
|
+
access_token: 'AT2',
|
|
680
|
+
scope: 'scope2',
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
credentials.set({
|
|
684
|
+
supertoken: st,
|
|
685
|
+
userTokens: [t1, t2],
|
|
686
|
+
});
|
|
687
|
+
|
|
688
|
+
return new Promise((resolve) => {
|
|
689
|
+
setTimeout(resolve, 1);
|
|
690
|
+
clock.tick(1000);
|
|
691
|
+
})
|
|
692
|
+
.then(() => webex.boundedStorage.get('Credentials', '@'))
|
|
693
|
+
.then((data) => {
|
|
694
|
+
assert.equal(data.userTokens[0].access_token, t1.access_token);
|
|
695
|
+
assert.equal(data.userTokens[1].access_token, t2.access_token);
|
|
696
|
+
|
|
697
|
+
return credentials.invalidate();
|
|
698
|
+
})
|
|
699
|
+
.then(() => promiseTick(500))
|
|
700
|
+
.then(
|
|
701
|
+
() =>
|
|
702
|
+
new Promise((resolve) => {
|
|
703
|
+
setTimeout(resolve, 1);
|
|
704
|
+
clock.tick(1000);
|
|
705
|
+
})
|
|
706
|
+
)
|
|
707
|
+
.then(() => promiseTick(500))
|
|
708
|
+
.then(
|
|
709
|
+
() =>
|
|
710
|
+
new Promise((resolve) => {
|
|
711
|
+
setTimeout(resolve, 1);
|
|
712
|
+
clock.tick(1000);
|
|
713
|
+
})
|
|
714
|
+
)
|
|
715
|
+
.then(() => assert.isRejected(webex.boundedStorage.get('Credentials', '@'), /NotFound/));
|
|
716
|
+
});
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
// it('does not induce any token refreshes');
|
|
720
|
+
|
|
721
|
+
it('prevents #getUserToken() from being invoked', () => {
|
|
722
|
+
const webex = new MockWebex();
|
|
723
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
724
|
+
|
|
725
|
+
webex.trigger('change:config');
|
|
726
|
+
const st = makeToken(webex, {
|
|
727
|
+
access_token: 'ST',
|
|
728
|
+
refresh_token: 'RT',
|
|
729
|
+
});
|
|
730
|
+
|
|
731
|
+
const t1 = makeToken(webex, {
|
|
732
|
+
access_token: 'AT1',
|
|
733
|
+
scope: 'scope1',
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
credentials.set({
|
|
737
|
+
supertoken: st,
|
|
738
|
+
userTokens: [t1],
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
return credentials
|
|
742
|
+
.invalidate()
|
|
743
|
+
.then(() =>
|
|
744
|
+
assert.isRejected(
|
|
745
|
+
credentials.getUserToken(),
|
|
746
|
+
/Current state cannot produce an access token/
|
|
747
|
+
)
|
|
748
|
+
);
|
|
749
|
+
});
|
|
750
|
+
});
|
|
751
|
+
|
|
752
|
+
describe('#refresh()', () => {
|
|
753
|
+
it('refreshes and downscopes the supertoken, and revokes previous tokens', () => {
|
|
754
|
+
const webex = new MockWebex();
|
|
755
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
756
|
+
|
|
757
|
+
webex.trigger('change:config');
|
|
758
|
+
const st = makeToken(webex, {
|
|
759
|
+
access_token: 'ST',
|
|
760
|
+
refresh_token: 'RT',
|
|
761
|
+
});
|
|
762
|
+
|
|
763
|
+
const st2 = makeToken(webex, {
|
|
764
|
+
access_token: 'ST2',
|
|
765
|
+
refresh_token: 'RT2',
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
const t1 = makeToken(webex, {
|
|
769
|
+
access_token: 'AT1',
|
|
770
|
+
scope: 'scope1',
|
|
771
|
+
});
|
|
772
|
+
|
|
773
|
+
const t2 = makeToken(webex, {
|
|
774
|
+
access_token: 'AT2',
|
|
775
|
+
scope: 'scope2',
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
779
|
+
sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
|
|
780
|
+
sinon.stub(t1, 'revoke').returns(Promise.resolve());
|
|
781
|
+
sinon.spy(credentials, 'scheduleRefresh');
|
|
782
|
+
|
|
783
|
+
credentials.set({
|
|
784
|
+
supertoken: st,
|
|
785
|
+
userTokens: [t1],
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
assert.equal(credentials.userTokens.get(t1.scope), t1);
|
|
789
|
+
|
|
790
|
+
return credentials
|
|
791
|
+
.refresh()
|
|
792
|
+
.then(() => assert.called(st.refresh))
|
|
793
|
+
.then(() => assert.calledWith(st2.downscope, 'scope1'))
|
|
794
|
+
.then(() => assert.called(t1.revoke))
|
|
795
|
+
.then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
|
|
796
|
+
.then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
|
|
797
|
+
.then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
|
|
798
|
+
});
|
|
799
|
+
|
|
800
|
+
it('refreshes and downscopes the supertoken even if revocation of previous token fails', () => {
|
|
801
|
+
const webex = new MockWebex();
|
|
802
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
803
|
+
|
|
804
|
+
webex.trigger('change:config');
|
|
805
|
+
const st = makeToken(webex, {
|
|
806
|
+
access_token: 'ST',
|
|
807
|
+
refresh_token: 'RT',
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
const st2 = makeToken(webex, {
|
|
811
|
+
access_token: 'ST2',
|
|
812
|
+
refresh_token: 'RT2',
|
|
813
|
+
});
|
|
814
|
+
|
|
815
|
+
const t1 = makeToken(webex, {
|
|
816
|
+
access_token: 'AT1',
|
|
817
|
+
scope: 'scope1',
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
const t2 = makeToken(webex, {
|
|
821
|
+
access_token: 'AT2',
|
|
822
|
+
scope: 'scope2',
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
826
|
+
sinon.stub(st, 'refresh').returns(Promise.resolve(st2));
|
|
827
|
+
sinon.stub(t1, 'revoke').returns(Promise.reject());
|
|
828
|
+
sinon.spy(credentials, 'scheduleRefresh');
|
|
829
|
+
|
|
830
|
+
credentials.set({
|
|
831
|
+
supertoken: st,
|
|
832
|
+
userTokens: [t1],
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
assert.equal(credentials.userTokens.get(t1.scope), t1);
|
|
836
|
+
|
|
837
|
+
return credentials
|
|
838
|
+
.refresh()
|
|
839
|
+
.then(() => assert.called(st.refresh))
|
|
840
|
+
.then(() => assert.calledWith(st2.downscope, 'scope1'))
|
|
841
|
+
.then(() => assert.called(t1.revoke))
|
|
842
|
+
.then(() => assert.isUndefined(credentials.userTokens.get(t1.scope)))
|
|
843
|
+
.then(() => assert.equal(credentials.userTokens.get(t2.scope), t2))
|
|
844
|
+
.then(() => assert.calledWith(credentials.scheduleRefresh, st.expires));
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
it('removes and revokes all child tokens', () => {
|
|
848
|
+
const webex = new MockWebex({
|
|
849
|
+
children: {
|
|
850
|
+
logger: Logger,
|
|
851
|
+
},
|
|
852
|
+
});
|
|
853
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
854
|
+
|
|
855
|
+
webex.trigger('change:config');
|
|
856
|
+
const st = makeToken(webex, {
|
|
857
|
+
access_token: 'ST',
|
|
858
|
+
refresh_token: 'RT',
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
sinon.stub(st, 'refresh').returns(Promise.resolve(makeToken(webex, {access_token: 'ST2'})));
|
|
862
|
+
|
|
863
|
+
const t1 = makeToken(webex, {
|
|
864
|
+
access_token: 'AT1',
|
|
865
|
+
scope: 'scope1',
|
|
866
|
+
});
|
|
867
|
+
|
|
868
|
+
credentials.set({
|
|
869
|
+
supertoken: st,
|
|
870
|
+
userTokens: [t1],
|
|
871
|
+
});
|
|
872
|
+
|
|
873
|
+
return credentials.refresh().then(() => assert.called(st.refresh));
|
|
874
|
+
});
|
|
875
|
+
|
|
876
|
+
it('allows #getUserToken() to be revoked, but #getUserToken() promises will not resolve until the suport token has been refreshed', () => {
|
|
877
|
+
const webex = new MockWebex();
|
|
878
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
879
|
+
|
|
880
|
+
webex.trigger('change:config');
|
|
881
|
+
const st1 = makeToken(webex, {
|
|
882
|
+
access_token: 'ST1',
|
|
883
|
+
refresh_token: 'RT1',
|
|
884
|
+
});
|
|
885
|
+
|
|
886
|
+
const st2 = makeToken(webex, {
|
|
887
|
+
access_token: 'ST2',
|
|
888
|
+
refresh_token: 'RT1',
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
const t1 = makeToken(webex, {
|
|
892
|
+
access_token: 'AT1',
|
|
893
|
+
scope: 'scope1',
|
|
894
|
+
});
|
|
895
|
+
|
|
896
|
+
const t2 = makeToken(webex, {
|
|
897
|
+
access_token: 'AT2',
|
|
898
|
+
scope: 'scope1',
|
|
899
|
+
});
|
|
900
|
+
|
|
901
|
+
sinon.stub(st1, 'refresh').returns(Promise.resolve(st2));
|
|
902
|
+
sinon.stub(st2, 'downscope').returns(Promise.resolve(t2));
|
|
903
|
+
|
|
904
|
+
credentials.set({
|
|
905
|
+
supertoken: st1,
|
|
906
|
+
userTokens: [t1],
|
|
907
|
+
});
|
|
908
|
+
|
|
909
|
+
credentials.refresh();
|
|
910
|
+
|
|
911
|
+
return credentials.getUserToken('scope1').then((result) => assert.deepEqual(result, t2));
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
it('emits InvalidRequestError when the refresh token and access token expire', () => {
|
|
915
|
+
const webex = new MockWebex();
|
|
916
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
917
|
+
|
|
918
|
+
webex.trigger('change:config');
|
|
919
|
+
const st = makeToken(webex, {
|
|
920
|
+
access_token: 'ST',
|
|
921
|
+
refresh_token: 'RT',
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
const t1 = makeToken(webex, {
|
|
925
|
+
access_token: 'AT1',
|
|
926
|
+
scope: 'scope1',
|
|
927
|
+
});
|
|
928
|
+
|
|
929
|
+
const res = {
|
|
930
|
+
body: {
|
|
931
|
+
error: 'invalid_request',
|
|
932
|
+
error_description:
|
|
933
|
+
'The refresh token provided is expired, revoked, malformed, or invalid.',
|
|
934
|
+
trackingID: 'test123',
|
|
935
|
+
},
|
|
936
|
+
};
|
|
937
|
+
|
|
938
|
+
const ErrorConstructor = grantErrors.select(res.body.error);
|
|
939
|
+
|
|
940
|
+
sinon
|
|
941
|
+
.stub(st, 'refresh')
|
|
942
|
+
.returns(Promise.reject(new ErrorConstructor('InvalidRequestError')));
|
|
943
|
+
sinon.stub(credentials, 'unset').returns(Promise.resolve());
|
|
944
|
+
const triggerSpy = sinon.spy(webex, 'trigger');
|
|
945
|
+
|
|
946
|
+
credentials.set({
|
|
947
|
+
supertoken: st,
|
|
948
|
+
userTokens: [t1],
|
|
949
|
+
});
|
|
950
|
+
|
|
951
|
+
return credentials
|
|
952
|
+
.refresh()
|
|
953
|
+
.then(() => assert.called(st.refresh))
|
|
954
|
+
.catch(() => {
|
|
955
|
+
assert.called(credentials.unset);
|
|
956
|
+
assert.calledWith(triggerSpy, sinon.match('client:InvalidRequestError'));
|
|
957
|
+
});
|
|
958
|
+
});
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
describe('#scheduleRefresh()', () => {
|
|
962
|
+
it('refreshes token immediately if token is expired', () => {
|
|
963
|
+
const webex = new MockWebex();
|
|
964
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
965
|
+
|
|
966
|
+
webex.trigger('change:config');
|
|
967
|
+
const st = makeToken(webex, {
|
|
968
|
+
access_token: 'ST',
|
|
969
|
+
refresh_token: 'RT',
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const st2 = makeToken(webex, {
|
|
973
|
+
access_token: 'ST2',
|
|
974
|
+
refresh_token: 'RT2',
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
credentials.set({
|
|
978
|
+
supertoken: st,
|
|
979
|
+
});
|
|
980
|
+
|
|
981
|
+
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
982
|
+
|
|
983
|
+
credentials.scheduleRefresh(Date.now() - 10000);
|
|
984
|
+
assert.isUndefined(credentials.refreshTimer);
|
|
985
|
+
assert.called(credentials.refresh);
|
|
986
|
+
});
|
|
987
|
+
|
|
988
|
+
it('schedules a token refresh', () => {
|
|
989
|
+
const webex = new MockWebex();
|
|
990
|
+
const credentials = new Credentials(undefined, {parent: webex});
|
|
991
|
+
|
|
992
|
+
webex.trigger('change:config');
|
|
993
|
+
const st = makeToken(webex, {
|
|
994
|
+
access_token: 'ST',
|
|
995
|
+
refresh_token: 'RT',
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
const st2 = makeToken(webex, {
|
|
999
|
+
access_token: 'ST2',
|
|
1000
|
+
refresh_token: 'RT2',
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
credentials.set({
|
|
1004
|
+
supertoken: st,
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
sinon.stub(credentials, 'refresh').returns(Promise.resolve(st2));
|
|
1008
|
+
|
|
1009
|
+
credentials.scheduleRefresh(Date.now() + 10000);
|
|
1010
|
+
assert.isDefined(credentials.refreshTimer);
|
|
1011
|
+
assert.notCalled(credentials.refresh);
|
|
1012
|
+
clock.tick(10000);
|
|
1013
|
+
assert.called(credentials.refresh);
|
|
1014
|
+
});
|
|
1015
|
+
});
|
|
1016
|
+
});
|
|
1017
|
+
});
|