Skip to content

Commit b46c80e

Browse files
committed
fix: Prevent duplicate PAT requests
1 parent 5a80d73 commit b46c80e

2 files changed

Lines changed: 30 additions & 19 deletions

File tree

packages/css/src/uma/UmaClient.ts

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ interface TokenResponse {
5757
export type UmaVerificationOptions = Omit<JWTVerifyOptions, 'iss' | 'aud' | 'sub' | 'iat'>;
5858

5959
const UMA_DISCOVERY = '/.well-known/uma2-configuration';
60+
const PAT_EVENT = 'PAT_EVENT';
6061

6162
const REQUIRED_METADATA = [
6263
'issuer',
@@ -75,10 +76,12 @@ const REQUIRED_METADATA = [
7576
export class UmaClient implements SingleThreaded {
7677
protected readonly logger = getLoggerFor(this);
7778

78-
// Keeps track of resources that are being registered to prevent duplicate registration calls.
79-
protected readonly inProgressResources: Set<string> = new Set();
80-
// Used to notify when registration finished for a resource. The event will be the identifier of the resource.
81-
protected readonly registerEmitter: EventEmitter = new EventEmitter();
79+
/* Keeps track of resources that are being registered to prevent duplicate registration calls.
80+
* Also keeps track if a PAT registration is going on. */
81+
protected readonly inProgress: Set<string> = new Set();
82+
/* Used to notify when registration finished for a resource. The event will be the identifier of the resource.
83+
* Also used to notify when a PAT was acquired. */
84+
protected readonly emitter: EventEmitter = new EventEmitter();
8285

8386
protected readonly configCache: NodeJS.Dict<{ config: UmaConfig, expiration: number }> = {};
8487
protected readonly patStorage: NodeJS.Dict<{ pat: string, expiration: number }> = {};
@@ -101,14 +104,19 @@ export class UmaClient implements SingleThreaded {
101104
) {
102105
// This number can potentially get very big when seeding a bunch of pods.
103106
// This is not really an issue, but it is still preferable to not have a warning printed.
104-
this.registerEmitter.setMaxListeners(20);
107+
this.emitter.setMaxListeners(20);
105108
}
106109

107110
public async getPat(issuer: string, credentials: string): Promise<string> {
108111
const cached = this.patStorage[credentials];
109112
if (cached && cached.expiration > Date.now()) {
110113
return cached.pat;
111114
}
115+
if (this.inProgress.has(PAT_EVENT)) {
116+
await once(this.emitter, PAT_EVENT);
117+
return this.getPat(issuer, credentials);
118+
}
119+
this.inProgress.add(PAT_EVENT);
112120

113121
const config = await this.fetchUmaConfig(issuer);
114122
const response = await this.fetcher.fetch(config.token_endpoint, {
@@ -129,6 +137,9 @@ export class UmaClient implements SingleThreaded {
129137
const expiration = Date.now() + expires_in * 1000;
130138
this.patStorage[credentials] = { pat, expiration };
131139

140+
this.inProgress.delete(PAT_EVENT);
141+
this.emitter.emit(PAT_EVENT);
142+
132143
return pat;
133144
}
134145

@@ -170,13 +181,13 @@ export class UmaClient implements SingleThreaded {
170181
const body = [];
171182
for (const [ target, modes ] of permissions.entrySets()) {
172183
let umaId = await this.umaIdStore.get(target.path);
173-
if (!umaId && this.inProgressResources.has(target.path)) {
184+
if (!umaId && this.inProgress.has(target.path)) {
174185
// Wait for the resource to finish registration if it is still being registered, and there is no UMA ID yet.
175186
// Time out after 2s to prevent getting stuck in case something goes wrong during registration.
176187
const timeoutPromise = promises.setTimeout(2000, '').then(() => {
177188
throw new InternalServerError(`Unable to finish registration for ${target.path}.`)
178189
});
179-
await Promise.race([timeoutPromise, once(this.registerEmitter, target.path)]);
190+
await Promise.race([timeoutPromise, once(this.emitter, target.path)]);
180191
umaId = await this.umaIdStore.get(target.path);
181192
}
182193
if (!umaId) {
@@ -350,14 +361,14 @@ export class UmaClient implements SingleThreaded {
350361
* and updated with the relations once the parent registration is finished.
351362
*/
352363
public async registerResource(resource: ResourceIdentifier, issuer: string, credentials: string): Promise<void> {
353-
if (this.inProgressResources.has(resource.path)) {
364+
if (this.inProgress.has(resource.path)) {
354365
// It is possible a resource is still being registered when an updated registration is already requested.
355366
// To prevent duplicate registrations of the same resource,
356367
// the next call will only happen when the first one is finished.
357-
await once(this.registerEmitter, resource.path);
368+
await once(this.emitter, resource.path);
358369
return this.registerResource(resource, issuer, credentials);
359370
}
360-
this.inProgressResources.add(resource.path);
371+
this.inProgress.add(resource.path);
361372
let { resource_registration_endpoint: endpoint } = await this.fetchUmaConfig(issuer);
362373
const knownUmaId = await this.umaIdStore.get(resource.path);
363374
if (knownUmaId) {
@@ -394,12 +405,12 @@ export class UmaClient implements SingleThreaded {
394405
resource.path} due to missing parent ID. Waiting for parent registration.`);
395406

396407
promises.push(
397-
once(this.registerEmitter, parentIdentifier.path)
408+
once(this.emitter, parentIdentifier.path)
398409
.then(() => this.registerResource(resource, issuer, credentials)),
399410
);
400411
// It is possible the parent is not yet being registered.
401412
// We need to force a registration in such a case, otherwise the above event will never be fired.
402-
if (!this.inProgressResources.has(parentIdentifier.path)) {
413+
if (!this.inProgress.has(parentIdentifier.path)) {
403414
promises.push(this.registerResource(parentIdentifier, issuer, credentials));
404415
}
405416
}
@@ -439,8 +450,8 @@ export class UmaClient implements SingleThreaded {
439450
this.logger.info(`Registered resource ${resource.path} with UMA ID ${umaId}`);
440451
}
441452
// Indicate this resource finished registration
442-
this.inProgressResources.delete(resource.path);
443-
this.registerEmitter.emit(resource.path);
453+
this.inProgress.delete(resource.path);
454+
this.emitter.emit(resource.path);
444455
});
445456

446457
// Execute all the required promises.

packages/css/test/unit/uma/UmaClient.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ import { Fetcher } from '../../../src/util/fetch/Fetcher';
1717
type Writeable<T> = { -readonly [P in keyof T]: T[P] };
1818

1919
class PublicUmaClient extends UmaClient {
20-
public inProgressResources: Set<string> = new Set();
21-
public registerEmitter: EventEmitter = new EventEmitter();
20+
public inProgress: Set<string> = new Set();
21+
public emitter: EventEmitter = new EventEmitter();
2222
public configCache: NodeJS.Dict<{ config: UmaConfig, expiration: number }> = {};
2323
public patStorage: NodeJS.Dict<{ pat: string, expiration: number }> = {};
2424
}
@@ -257,11 +257,11 @@ describe('UmaClient', (): void => {
257257
umaIdStore.get.mockResolvedValueOnce('uma2');
258258

259259
const publicClient = new PublicUmaClient(umaIdStore, fetcher, identifierStrategy, resourceSet, baseUrl);
260-
publicClient.inProgressResources.add('target1');
260+
publicClient.inProgress.add('target1');
261261
const prom = publicClient.fetchTicket(permissions, issuer, credentials);
262262
await flushPromises();
263263
vi.advanceTimersByTime(1000);
264-
publicClient.registerEmitter.emit('target1');
264+
publicClient.emitter.emit('target1');
265265

266266
await expect(prom).resolves.toBeUndefined();
267267
expect(fetcher.fetch).toHaveBeenNthCalledWith(3, umaConfig.permission_endpoint, {
@@ -287,7 +287,7 @@ describe('UmaClient', (): void => {
287287
umaIdStore.get.mockResolvedValueOnce('uma2');
288288

289289
const publicClient = new PublicUmaClient(umaIdStore, fetcher, identifierStrategy, resourceSet, baseUrl);
290-
publicClient.inProgressResources.add('target1');
290+
publicClient.inProgress.add('target1');
291291
const prom = publicClient.fetchTicket(permissions, issuer, credentials);
292292
await flushPromises();
293293
vi.advanceTimersByTime(3000);

0 commit comments

Comments
 (0)