-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathMainAuthorizationClient.test.ts
More file actions
300 lines (241 loc) · 12.2 KB
/
MainAuthorizationClient.test.ts
File metadata and controls
300 lines (241 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
import type { AuthorizationServiceConfiguration, TokenRequest } from "@openid/appauth";
import { BaseTokenRequestHandler } from "@openid/appauth";
import chai, { assert, expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import sinon from "sinon";
import { ElectronMainAuthorization } from "../main/Client";
import { RefreshTokenStore } from "../main/TokenStore";
import { getConfig, getMockTokenResponse, setupMockAuthServer, stubTokenCrypto } from "./helpers/testHelper";
/* eslint-disable @typescript-eslint/naming-convention */
chai.use(chaiAsPromised);
/**
* Produces a token store key using the same method as the Electron client
*/
function getTokenStoreKey(clientId: string, issuerUrl?: string): string {
let prefix = process.env.IMJS_URL_PREFIX;
const authority = new URL(issuerUrl ?? "https://ims.bentley.com");
if (prefix && !issuerUrl) {
prefix = prefix === "dev-" ? "qa-" : prefix;
authority.hostname = prefix + authority.hostname;
}
issuerUrl = authority.href.replace(/\/$/, "");
return `${getTokenStoreFileName(clientId)}:${issuerUrl}`;
}
function getTokenStoreFileName(clientId: string): string {
return `iTwinJs_${clientId}`;
}
describe("ElectronMainAuthorization Token Logic", () => {
beforeEach(function () {
sinon.restore();
// Stub Electron calls
sinon.stub(ElectronMainAuthorization.prototype, "setupIPCHandlers" as any);
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenChange" as any);
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenExpirationChange" as any);
});
it("Should throw if not signed in", async () => {
const client = new ElectronMainAuthorization({
clientId: "testClientId",
scopes: "testScope",
redirectUris: ["testRedirectUri_1", "testRedirectUri_2"],
});
// eslint-disable-next-line @typescript-eslint/unbound-method
chai.expect(client.getAccessToken).to.be.throw;
});
it("Should load token response from token store", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse({ accessToken: "testAccessToken" });
const refreshToken = "old refresh token";
stubTokenCrypto(refreshToken);
// Load refresh token into token store - use clientId
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
await tokenStore.save(refreshToken);
const spy = await setupMockAuthServer(mockTokenResponse);
// Create client and silent sign in
const client = new ElectronMainAuthorization(config);
await client.signInSilent();
// Get access token and assert its response equals what mock
const returnedToken = await client.getAccessToken();
assert.equal(returnedToken, `bearer ${mockTokenResponse.accessToken}`);
sinon.assert.notCalled(spy);
});
it("Should sign in", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse();
stubTokenCrypto(mockTokenResponse.refreshToken!);
// Clear token store
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
await tokenStore.delete();
await setupMockAuthServer(mockTokenResponse);
// Create client and call initialize
const client = new ElectronMainAuthorization(config);
await client.signIn();
const token = await client.getAccessToken();
assert.equal(token, `bearer ${mockTokenResponse.accessToken}`);
});
it("Should refresh old token", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse();
const refreshToken = "old refresh token";
stubTokenCrypto(refreshToken);
// Load refresh token into token store - use clientId
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
await tokenStore.save(refreshToken);
await setupMockAuthServer(mockTokenResponse);
// Create client and silent signin
const client = new ElectronMainAuthorization(config);
await client.signInSilent();
// TODO: Need cleaner way to reset just one method (performTokenRequest)
sinon.restore();
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenChange" as any);
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenExpirationChange" as any);
sinon.stub(BaseTokenRequestHandler.prototype, "performTokenRequest").callsFake(async (_configuration: AuthorizationServiceConfiguration, _request: TokenRequest) => {
return mockTokenResponse;
});
// Get access token and assert its response equals what mock
const returnedToken = await client.getAccessToken();
assert.equal(returnedToken, `bearer ${mockTokenResponse.accessToken}`);
});
it("should save new refresh token after signIn() when no electron-store token is present", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse();
stubTokenCrypto(mockTokenResponse.refreshToken!);
// Clear token store
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
await tokenStore.delete();
await setupMockAuthServer(mockTokenResponse, {
performTokenRequestCb: async () => {
await tokenStore.save(mockTokenResponse.refreshToken!);
},
});
const saveSpy = sinon.spy(tokenStore, "save");
// Create client and call initialize
const client = new ElectronMainAuthorization(config);
await client.signIn();
const token = await client.getAccessToken();
assert.equal(token, `bearer ${mockTokenResponse.accessToken}`);
assert.isTrue(saveSpy.calledOnce);
});
it("should load and decrypt refresh token on signIn() given an existing refresh token in electron-store", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse({ accessToken: "testAccessToken" });
const refreshToken = "old refresh token";
const { decryptSpy } = stubTokenCrypto(refreshToken);
// Load refresh token into token store - use clientId
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
await tokenStore.delete();
await tokenStore.save(refreshToken);
const spy = await setupMockAuthServer(mockTokenResponse);
// Create client and silent sign in
const client = new ElectronMainAuthorization(config);
await client.signIn();
sinon.assert.notCalled(spy);
assert.isTrue(decryptSpy.calledOnce);
});
});
describe("ElectronMainAuthorization Authority URL Logic", () => {
beforeEach(() => {
sinon.restore();
sinon.stub(ElectronMainAuthorization.prototype, "setupIPCHandlers" as any);
});
const config = getConfig();
const testAuthority = "https://test.authority.com";
it("should use config authority without prefix", async () => {
process.env.IMJS_URL_PREFIX = "";
const client = new ElectronMainAuthorization({ ...config, issuerUrl: testAuthority });
expect(client.issuerUrl).equals(testAuthority);
});
it("should use config authority and ignore prefix", async () => {
process.env.IMJS_URL_PREFIX = "prefix-";
const client = new ElectronMainAuthorization({ ...config, issuerUrl: testAuthority });
expect(client.issuerUrl).equals("https://test.authority.com");
});
it("should use default authority without prefix ", async () => {
process.env.IMJS_URL_PREFIX = "";
const client = new ElectronMainAuthorization(config);
expect(client.issuerUrl).equals("https://ims.bentley.com");
});
it("should use default authority with prefix ", async () => {
process.env.IMJS_URL_PREFIX = "prefix-";
const client = new ElectronMainAuthorization(config);
expect(client.issuerUrl).equals("https://prefix-ims.bentley.com");
});
it("should reroute dev prefix to qa if on default ", async () => {
process.env.IMJS_URL_PREFIX = "dev-";
const client = new ElectronMainAuthorization(config);
expect(client.issuerUrl).equals("https://qa-ims.bentley.com");
});
});
describe("ElectronMainAuthorization Config Scope Logic", () => {
beforeEach(() => {
sinon.restore();
sinon.stub(ElectronMainAuthorization.prototype, "setupIPCHandlers" as any);
});
it("Should add offline_access scope", async () => {
const config = getConfig();
const client = new ElectronMainAuthorization(config);
expect(client.scopes).equals(`${config.scopes} offline_access`);
});
it("Should not add offline_access scope", async () => {
const client = new ElectronMainAuthorization({
clientId: "testClientId",
scopes: "testScope offline_access",
redirectUris: ["testRedirectUri_1", "testRedirectUri_2"],
});
expect(client.scopes).equals("testScope offline_access");
});
describe("scope changes", () => {
beforeEach(() => {
sinon.restore();
// Stub Electron calls
sinon.stub(ElectronMainAuthorization.prototype, "setupIPCHandlers" as any);
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenChange" as any);
sinon.stub(ElectronMainAuthorization.prototype, "notifyFrontendAccessTokenExpirationChange" as any);
});
it("delete the current refresh token", async () => {
const config = getConfig();
const mockTokenResponse = getMockTokenResponse();
stubTokenCrypto(mockTokenResponse.refreshToken!);
await setupMockAuthServer(mockTokenResponse);
const client = new ElectronMainAuthorization(config);
expect(client.scopes).equals(`${config.scopes} offline_access`);
await client.signIn();
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
const token = await tokenStore.load("testScope offline_access");
assert.equal(token, mockTokenResponse.refreshToken);
const _token = await tokenStore.load("differetnTestScope offline_access");
assert.equal(_token, undefined);
});
it("delete the current refresh token regardless of order", async () => {
const config = getConfig({ scopes: "testScope blurgh-platform ReadTHINGS" });
const mockTokenResponse = getMockTokenResponse();
stubTokenCrypto(mockTokenResponse.refreshToken!);
await setupMockAuthServer(mockTokenResponse);
const client = new ElectronMainAuthorization(config);
expect(client.scopes).equals("testScope blurgh-platform ReadTHINGS offline_access");
await client.signIn();
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
const token = await tokenStore.load("ReadTHINGS blurgh-platform offline_access testScope");
assert.equal(token, mockTokenResponse.refreshToken);
const _token = await tokenStore.load("ReadTHINGS offline_access testScope blurgh-platform new-scope");
assert.equal(_token, undefined);
});
it("delete the current refresh token works with explicit offline_access added", async () => {
const config = getConfig({ scopes: "testScope blurgh-platform offline_access ReadTHINGS" });
const mockTokenResponse = getMockTokenResponse();
stubTokenCrypto(mockTokenResponse.refreshToken!);
await setupMockAuthServer(mockTokenResponse);
const client = new ElectronMainAuthorization(config);
expect(client.scopes).equals("testScope blurgh-platform offline_access ReadTHINGS");
await client.signIn();
const tokenStore = new RefreshTokenStore(getTokenStoreFileName(config.clientId), getTokenStoreKey(config.clientId));
const token = await tokenStore.load("offline_access ReadTHINGS blurgh-platform testScope");
assert.equal(token, mockTokenResponse.refreshToken);
const _token = await tokenStore.load("ReadTHINGS offline_access testScope blurgh-platform new-scope");
assert.equal(_token, undefined);
});
});
});