From b9705e9439a92c5875c85a819448146138539c32 Mon Sep 17 00:00:00 2001 From: "Alex Yang (DevDiv)" Date: Mon, 20 Jul 2026 14:49:48 -0700 Subject: [PATCH] Fix concurrent token initialization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- extensions/connector-namespaces/auth.mjs | 52 +++++++++++-------- extensions/connector-namespaces/auth.test.mjs | 41 +++++++++++++++ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/extensions/connector-namespaces/auth.mjs b/extensions/connector-namespaces/auth.mjs index 30418d64..dc5b01fd 100644 --- a/extensions/connector-namespaces/auth.mjs +++ b/extensions/connector-namespaces/auth.mjs @@ -231,31 +231,35 @@ export class InteractiveAuthBroker { if (hasUsableToken(this.accessToken, this.now())) return this.accessToken.token; if (this.tokenInFlight) return this.tokenInFlight; - let credential = this.credential; - if (!credential) { - try { - const authenticationRecord = await this.loadAuthRecord(); - credential = this.createInteractiveCredential(authenticationRecord); - this.credential = credential; - } catch (error) { - if (isAuthenticationRequiredError(error)) { - throw new ConnectorAuthenticationRequiredError( - "Sign in to Azure to continue.", - { cause: error }, - ); + const request = (async () => { + let credential = this.credential; + if (!credential) { + try { + const authenticationRecord = await this.loadAuthRecord(); + if (hasUsableToken(this.accessToken, this.now())) return this.accessToken.token; + credential = this.credential; + if (!credential) { + credential = this.createInteractiveCredential(authenticationRecord); + this.credential = credential; + } + } catch (error) { + if (isAuthenticationRequiredError(error)) { + throw new ConnectorAuthenticationRequiredError( + "Sign in to Azure to continue.", + { cause: error }, + ); + } + throw error; } - throw error; } - } - const request = credential.getToken(this.scope) - .then((accessToken) => { + try { + const accessToken = await credential.getToken(this.scope); if (!accessToken?.token || !Number.isFinite(accessToken.expiresOnTimestamp)) { throw new Error("Azure identity returned an incomplete ARM access token."); } this.accessToken = accessToken; return accessToken.token; - }) - .catch((error) => { + } catch (error) { if (!isAuthenticationRequiredError(error)) throw error; if (this.credential === credential) { this.credential = null; @@ -265,12 +269,14 @@ export class InteractiveAuthBroker { "Sign in to Azure to continue.", { cause: error }, ); - }) - .finally(() => { - if (this.tokenInFlight === request) this.tokenInFlight = null; - }); + } + })(); this.tokenInFlight = request; - return request; + try { + return await request; + } finally { + if (this.tokenInFlight === request) this.tokenInFlight = null; + } } } diff --git a/extensions/connector-namespaces/auth.test.mjs b/extensions/connector-namespaces/auth.test.mjs index 89f551e6..608769b9 100644 --- a/extensions/connector-namespaces/auth.test.mjs +++ b/extensions/connector-namespaces/auth.test.mjs @@ -162,6 +162,47 @@ test("a new broker restores the persisted credential without reopening the brows assert.equal(authenticateCalls, 1); }); +test("concurrent first-time token requests share credential initialization and acquisition", async () => { + let releaseAuthenticationRecord; + const authenticationRecordReady = new Promise((resolve) => { + releaseAuthenticationRecord = resolve; + }); + let loadAuthRecordCalls = 0; + let createCredentialCalls = 0; + let tokenCalls = 0; + const broker = new InteractiveAuthBroker({ + async loadAuthRecord() { + loadAuthRecordCalls++; + await authenticationRecordReady; + return authenticationRecord(); + }, + createCredential: () => { + createCredentialCalls++; + return { + async getToken() { + tokenCalls++; + return accessToken("shared-token"); + }, + }; + }, + }); + + const firstRequest = broker.getToken(); + const secondRequest = broker.getToken(); + await new Promise((resolve) => setImmediate(resolve)); + const loadsBeforeRelease = loadAuthRecordCalls; + releaseAuthenticationRecord(); + + assert.deepEqual( + await Promise.all([firstRequest, secondRequest]), + ["shared-token", "shared-token"], + ); + assert.equal(loadsBeforeRelease, 1); + assert.equal(loadAuthRecordCalls, 1); + assert.equal(createCredentialCalls, 1); + assert.equal(tokenCalls, 1); +}); + test("cancelling sign-in aborts the credential request", async () => { let abortSignal; const credential = {