@adobe/helix-onedrive-support 6.2.2 → 7.0.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [7.0.0](https://github.com/adobe/helix-onedrive-support/compare/v6.2.2...v7.0.0) (2022-03-23)
2
+
3
+
4
+ ### Features
5
+
6
+ * add transparent tenant resolution ([dc59dbf](https://github.com/adobe/helix-onedrive-support/commit/dc59dbfc53d767593b82c845c753da3885560852))
7
+
8
+
9
+ ### BREAKING CHANGES
10
+
11
+ * API slightly refactored
12
+ - authorityUrl is now method: `getAuthorityIUrl`
13
+ - new method: `setAccessToken`
14
+
1
15
  ## [6.2.2](https://github.com/adobe/helix-onedrive-support/compare/v6.2.1...v6.2.2) (2022-03-20)
2
16
 
3
17
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/helix-onedrive-support",
3
- "version": "6.2.2",
3
+ "version": "7.0.0",
4
4
  "description": "Helix OneDrive Support",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -24,7 +24,8 @@
24
24
  "homepage": "https://github.com/adobe/helix-onedrive-support#readme",
25
25
  "dependencies": {
26
26
  "@adobe/helix-fetch": "3.0.7",
27
- "adal-node": "https://github.com/adobe-rnd/azure-activedirectory-library-for-nodejs.git#adobe"
27
+ "adal-node": "https://github.com/adobe-rnd/azure-activedirectory-library-for-nodejs.git#adobe",
28
+ "jose": "4.6.0"
28
29
  },
29
30
  "devDependencies": {
30
31
  "@adobe/eslint-config-helix": "1.3.2",
package/src/OneDrive.d.ts CHANGED
@@ -25,6 +25,7 @@ export declare interface OneDriveOptions {
25
25
  refreshToken?: string;
26
26
  log?: Logger;
27
27
  tenant?: string;
28
+ resource?: string;
28
29
  username?: string;
29
30
  password?: string;
30
31
 
@@ -44,6 +45,18 @@ export declare interface OneDriveOptions {
44
45
  * Note that the cache is only used, if the `noShareLinkCache` flag is `falsy`
45
46
  */
46
47
  shareLinkCache?: Map<string, DriveItem>,
48
+
49
+ /**
50
+ * Disables the cache for the tenant lookup.
51
+ * @default process.env.HELIX_ONEDRIVE_NO_TENANT_CACHE
52
+ */
53
+ noTenantCache?: boolean;
54
+
55
+ /**
56
+ * Map to use for the tenant lookup cache. If empty, a module-global cache will be used.
57
+ * Note that the cache is only used, if the `noTenantCache` flag is `falsy`
58
+ */
59
+ tenantCache?: Map<string, DriveItem>,
47
60
  }
48
61
 
49
62
  export declare interface GraphResult {
@@ -145,7 +158,7 @@ export declare class OneDrive extends EventEmitter {
145
158
  /**
146
159
  * the authority url for login.
147
160
  */
148
- authorityUrl: string;
161
+ getAuthorityUrl(): string;
149
162
 
150
163
  /**
151
164
  * Adds entries to the token cache
@@ -161,6 +174,14 @@ export declare class OneDrive extends EventEmitter {
161
174
  */
162
175
  login(onCode: Function): Promise<TokenResponse>;
163
176
 
177
+ /**
178
+ * Sets the access token to use for all requests. if the token is a valid JWT token,
179
+ * its `tid` claim is used a tenant (if no tenant is already set).
180
+ *
181
+ * @param {string} bearerToken
182
+ */
183
+ setAccessToken(bearerToken);
184
+
164
185
  getAccessToken(autoRefresh: boolean): Promise<TokenResponse>;
165
186
 
166
187
  createLoginUrl(): string;
package/src/OneDrive.js CHANGED
@@ -13,6 +13,7 @@
13
13
  // eslint-disable-next-line max-classes-per-file
14
14
  const EventEmitter = require('events');
15
15
  const { promisify } = require('util');
16
+ const jose = require('jose');
16
17
  const { AuthenticationContext, MemoryCache } = require('adal-node');
17
18
  const { fetch, reset } = require('@adobe/helix-fetch').keepAliveNoCache({ userAgent: 'helix-fetch' });
18
19
 
@@ -24,7 +25,7 @@ const SharePointSite = require('./SharePointSite.js');
24
25
 
25
26
  const AZ_AUTHORITY_HOST_URL = 'https://login.windows.net';
26
27
  const AZ_DEFAULT_RESOURCE = 'https://graph.microsoft.com'; // '00000002-0000-0000-c000-000000000000'; ??
27
- const AZ_DEFAULT_TENANT = 'common';
28
+ const AZ_COMMON_TENANT = 'common';
28
29
 
29
30
  /**
30
31
  * the maximum subscription time in milliseconds
@@ -37,27 +38,23 @@ const MAX_SUBSCRIPTION_EXPIRATION_TIME = 4230 * 60 * 1000;
37
38
 
38
39
  /**
39
40
  * map that caches share item data. key is a sharing url, the value a drive item.
40
- * @type {Map<string, *>}
41
+ * @type {Map<string, string>}
41
42
  * @private
42
43
  */
43
44
  const globalShareLinkCache = new Map();
44
45
 
46
+ /**
47
+ * map that caches the tenant ids
48
+ * @type {Map<string, string>}
49
+ */
50
+ const globalTenantCache = new Map();
51
+
45
52
  /**
46
53
  * Helper class that facilitates accessing one drive.
47
54
  */
48
55
  class OneDrive extends EventEmitter {
49
56
  /**
50
57
  * @param {OneDriveOptions} opts Options
51
- * @param {string} opts.clientId The client id of the app
52
- * @param {string} [opts.clientSecret] The client secret of the app
53
- * @param {string} [opts.refreshToken] The refresh token.
54
- * @param {string} [opts.accessToken] The access token.
55
- * @param {string} [opts.username] Username for username/password authentication.
56
- * @param {string} [opts.password] Password for username/password authentication.
57
- * @param {number} [opts.expiresOn] Expiration time.
58
- * @param {Logger} [opts.log] A logger.
59
- * @param {boolean} [opts.localAuthCache] Whether to use local auth cache
60
- * @param {string} [opts.resource] Azure resource to authenticate against. defaults to MS Graph.
61
58
  */
62
59
  constructor(opts) {
63
60
  super(opts);
@@ -67,53 +64,109 @@ class OneDrive extends EventEmitter {
67
64
  this.username = opts.username || '';
68
65
  this.password = opts.password || '';
69
66
  this._log = opts.log || console;
70
- this.tenant = opts.tenant || AZ_DEFAULT_TENANT;
67
+ this.tenant = opts.tenant;
71
68
  this.resource = opts.resource || AZ_DEFAULT_RESOURCE;
69
+ this.localAuthCache = opts.localAuthCache;
72
70
 
73
71
  if (!opts.noShareLinkCache && !process.env.HELIX_ONEDRIVE_NO_SHARE_LINK_CACHE) {
72
+ /** @type {Map<string, string>} */
74
73
  this.shareLinkCache = opts.shareLinkCache || globalShareLinkCache;
75
74
  }
75
+ if (!opts.noTenantCache && !process.env.HELIX_ONEDRIVE_NO_TENANT_CACHE) {
76
+ /** @type {Map<string, string>} */
77
+ this.tenantCache = opts.tenantCache || globalTenantCache;
78
+ }
76
79
 
77
80
  if (!this.clientId) {
78
81
  throw new Error('Missing clientId.');
79
82
  }
80
- this.authContext = new AuthenticationContext(
81
- this.authorityUrl,
82
- undefined,
83
- opts.localAuthCache ? new MemoryCache() : undefined,
84
- );
85
- [
86
- 'acquireUserCode',
87
- 'acquireToken',
88
- 'acquireTokenWithDeviceCode',
89
- 'acquireTokenWithRefreshToken',
90
- 'acquireTokenWithUsernamePassword',
91
- 'acquireTokenWithClientCredentials',
92
- ].forEach((m) => {
93
- this.authContext[m] = promisify(this.authContext[m].bind(this.authContext));
94
- });
95
- const { cache } = this.authContext;
96
- if (opts.localAuthCache) {
97
- const originalAdd = cache.add;
98
- cache.add = (entries, cb) => {
99
- originalAdd.call(cache, entries, (...args) => {
100
- // eslint-disable-next-line no-underscore-dangle
101
- this.emit('tokens', cache._entries);
102
- cb(...args);
103
- });
104
- };
105
- const originalRemove = cache.remove;
106
- cache.remove = (entries, cb) => {
107
- originalRemove.call(cache, entries, (...args) => {
108
- // eslint-disable-next-line no-underscore-dangle
109
- this.emit('tokens', cache._entries);
110
- cb(...args);
111
- });
112
- };
113
- }
114
- cache.add.promise = promisify(cache.add.bind(cache));
115
- cache.remove.promise = promisify(cache.remove.bind(cache));
116
- cache.find.promise = promisify(cache.find.bind(cache));
83
+ }
84
+
85
+ /**
86
+ * Return the auth context
87
+ * @returns {AuthenticationContext}
88
+ */
89
+ async getAuthContext() {
90
+ if (!this.authContext) {
91
+ this.authContext = new AuthenticationContext(
92
+ this.getAuthorityUrl(),
93
+ undefined,
94
+ this.localAuthCache ? new MemoryCache() : undefined,
95
+ );
96
+ [
97
+ 'acquireUserCode',
98
+ 'acquireToken',
99
+ 'acquireTokenWithDeviceCode',
100
+ 'acquireTokenWithRefreshToken',
101
+ 'acquireTokenWithUsernamePassword',
102
+ 'acquireTokenWithClientCredentials',
103
+ ].forEach((m) => {
104
+ this.authContext[m] = promisify(this.authContext[m].bind(this.authContext));
105
+ });
106
+ const { cache } = this.authContext;
107
+ if (this.localAuthCache) {
108
+ const originalAdd = cache.add;
109
+ cache.add = (entries, cb) => {
110
+ originalAdd.call(cache, entries, (...args) => {
111
+ // eslint-disable-next-line no-underscore-dangle
112
+ this.emit('tokens', cache._entries);
113
+ cb(...args);
114
+ });
115
+ };
116
+ const originalRemove = cache.remove;
117
+ cache.remove = (entries, cb) => {
118
+ originalRemove.call(cache, entries, (...args) => {
119
+ // eslint-disable-next-line no-underscore-dangle
120
+ this.emit('tokens', cache._entries);
121
+ cb(...args);
122
+ });
123
+ };
124
+ }
125
+ cache.add.promise = promisify(cache.add.bind(cache));
126
+ cache.remove.promise = promisify(cache.remove.bind(cache));
127
+ cache.find.promise = promisify(cache.find.bind(cache));
128
+ }
129
+ return this.authContext;
130
+ }
131
+
132
+ async resolveTenant(tenantHost) {
133
+ const { log } = this;
134
+ const configUrl = `https://login.windows.net/${tenantHost}.onmicrosoft.com/.well-known/openid-configuration`;
135
+ const res = await fetch(configUrl);
136
+ if (!res.ok) {
137
+ log.info(`error fetching openid-configuration for ${tenantHost}: ${res.status}. Fallback to 'common'`);
138
+ return AZ_COMMON_TENANT;
139
+ }
140
+
141
+ const { issuer } = await res.json();
142
+ if (!issuer) {
143
+ log.info(`unable to extract tenant from openid-configuration for ${tenantHost}: no 'issuer'. Fallback to 'common'`);
144
+ return AZ_COMMON_TENANT;
145
+ }
146
+
147
+ // eslint-disable-next-line prefer-destructuring
148
+ const tenant = new URL(issuer).pathname.split('/')[1];
149
+ log.info(`fetched tenant information from for ${tenantHost}: ${tenant}`);
150
+ return tenant;
151
+ }
152
+
153
+ async initTenantFromShareLink(sharingUrl) {
154
+ if (this.tenant) {
155
+ return;
156
+ }
157
+ const { log } = this;
158
+ const [tenantHost] = new URL(sharingUrl).hostname.split('.');
159
+
160
+ if (this.tenantCache) {
161
+ this.tenant = this.tenantCache.get(tenantHost);
162
+ }
163
+ if (!this.tenant) {
164
+ this.tenant = await this.resolveTenant(tenantHost);
165
+ if (this.tenantCache) {
166
+ this.tenantCache.set(tenantHost, this.tenant);
167
+ }
168
+ }
169
+ log.info(`using tenant ${this.tenant} for ${tenantHost} from ${sharingUrl}`);
117
170
  }
118
171
 
119
172
  /**
@@ -130,7 +183,10 @@ class OneDrive extends EventEmitter {
130
183
  return this._log;
131
184
  }
132
185
 
133
- get authorityUrl() {
186
+ getAuthorityUrl() {
187
+ if (!this.tenant) {
188
+ throw new Error('unable to compute authority url. no tenant.');
189
+ }
134
190
  return `${AZ_AUTHORITY_HOST_URL}/${this.tenant}`;
135
191
  }
136
192
 
@@ -139,7 +195,7 @@ class OneDrive extends EventEmitter {
139
195
  */
140
196
  get authenticated() {
141
197
  // eslint-disable-next-line no-underscore-dangle
142
- return this.authContext.cache._entries.length > 0;
198
+ return this.authContext?.cache._entries.length > 0;
143
199
  }
144
200
 
145
201
  /**
@@ -148,7 +204,7 @@ class OneDrive extends EventEmitter {
148
204
  * @return this;
149
205
  */
150
206
  async loadTokenCache(entries) {
151
- return this.authContext.cache.add.promise(entries);
207
+ return (await this.getAuthContext()).cache.add.promise(entries);
152
208
  }
153
209
 
154
210
  /**
@@ -158,7 +214,8 @@ class OneDrive extends EventEmitter {
158
214
  * @returns {Promise<TokenResponse>}
159
215
  */
160
216
  async login(onCode) {
161
- const { log, authContext: context } = this;
217
+ const { log } = this;
218
+ const context = await this.getAuthContext();
162
219
 
163
220
  let code;
164
221
  try {
@@ -182,9 +239,35 @@ class OneDrive extends EventEmitter {
182
239
  }
183
240
 
184
241
  /**
242
+ * Sets the access token to use for all requests. if the token is a valid JWT token,
243
+ * its `tid` claim is used a tenant (if no tenant is already set).
244
+ *
245
+ * @param {string} bearerToken
185
246
  */
186
- async getAccessToken() {
187
- const { log, authContext: context } = this;
247
+ setAccessToken(bearerToken) {
248
+ const { log } = this;
249
+ this.accessToken = {
250
+ accessToken: bearerToken,
251
+ };
252
+ if (!this.tenant) {
253
+ try {
254
+ const { tid } = jose.decodeJwt(bearerToken);
255
+ if (tid) {
256
+ log.info(`using tenant from access token: ${tid}`);
257
+ this.tenant = tid;
258
+ }
259
+ } catch (e) {
260
+ log.warn(`unable to decode access token: ${e.message}`);
261
+ }
262
+ }
263
+ this.accessToken.tenantId = this.tenant;
264
+ }
265
+
266
+ /**
267
+ */
268
+ async fetchAccessToken() {
269
+ const { log } = this;
270
+ const context = await this.getAuthContext();
188
271
  try {
189
272
  return await context.acquireToken(this.resource, this.username, this.clientId);
190
273
  } catch (e) {
@@ -231,10 +314,17 @@ class OneDrive extends EventEmitter {
231
314
  }
232
315
  }
233
316
 
317
+ async getAccessToken() {
318
+ if (!this.accessToken) {
319
+ this.accessToken = await this.fetchAccessToken();
320
+ }
321
+ return this.accessToken;
322
+ }
323
+
234
324
  /**
235
325
  */
236
326
  createLoginUrl(redirectUri, state) {
237
- return `${this.authorityUrl}/oauth2/authorize?response_type=code&scope=/.default&client_id=${this.clientId}&redirect_uri=${redirectUri}&state=${state}&resource=${this.resource}`;
327
+ return `${this.getAuthorityUrl()}/oauth2/authorize?response_type=code&scope=/.default&client_id=${this.clientId}&redirect_uri=${redirectUri}&state=${state}&resource=${this.resource}`;
238
328
  }
239
329
 
240
330
  async augmentAndCacheResponse(response) {
@@ -259,7 +349,8 @@ class OneDrive extends EventEmitter {
259
349
  /**
260
350
  */
261
351
  async acquireToken(redirectUri, code) {
262
- const { log, authContext: context } = this;
352
+ const { log } = this;
353
+ const context = await this.getAuthContext();
263
354
  try {
264
355
  const resp = await context.acquireTokenWithAuthorizationCode(
265
356
  code,
@@ -342,6 +433,7 @@ class OneDrive extends EventEmitter {
342
433
  /**
343
434
  */
344
435
  async resolveShareLink(sharingUrl) {
436
+ await this.initTenantFromShareLink(sharingUrl);
345
437
  const link = OneDrive.encodeSharingUrl(sharingUrl);
346
438
  this.log.debug(`resolving sharelink ${sharingUrl} (${link})`);
347
439
  try {
@@ -368,6 +460,7 @@ class OneDrive extends EventEmitter {
368
460
  if (driveItem) {
369
461
  return driveItem;
370
462
  }
463
+ await this.initTenantFromShareLink(sharingUrl);
371
464
  if (this.shareLinkCache) {
372
465
  driveItem = this.shareLinkCache.get(sharingUrl);
373
466
  }