Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions auth/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Change Log

## 5.1.1 - 2025-10-28

* [#2111](https://github.com/microsoft/vscode-azuretools/pull/2111) Same as https://github.com/microsoft/vscode-azuretools/pull/2110 but a better fix.

## 5.1.0 - 2025-10-27

* [#2102](https://github.com/microsoft/vscode-azuretools/pull/2102) Fixes an issue causing infinite event loops especially in https://vscode.dev/azure
Expand Down
4 changes: 2 additions & 2 deletions auth/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion auth/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@microsoft/vscode-azext-azureauth",
"author": "Microsoft Corporation",
"version": "5.1.0",
"version": "5.1.1",
"description": "Azure authentication helpers for Visual Studio Code",
"tags": [
"azure",
Expand Down
123 changes: 68 additions & 55 deletions auth/src/VSCodeAzureSubscriptionProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,82 +25,95 @@ let armSubs: typeof import('@azure/arm-resources-subscriptions') | undefined;
* provider.
*/
export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvider, vscode.Disposable {
private lastSignInEventFired: number = 0;
private suppressSignInEvents: boolean = false;
private listenerDisposable: vscode.Disposable | undefined;

private lastSignOutEventFired: number = 0;
private readonly onDidSignInEmitter = new vscode.EventEmitter<void>();
private readonly onDidSignOutEmitter = new vscode.EventEmitter<void>();
private lastEventFired: number = 0;
private suppressEvents: boolean = false;
Comment on lines +32 to +33
Copy link
Contributor Author

@bwateratmsft bwateratmsft Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up having to combine the timers/suppression. I can't make it work with two separate debounce timers--there's always some way things go wrong. However, I think it's fine--it's not likely the user is simultaneously signing in and signing out.


private priorAccounts: vscode.AuthenticationSessionAccountInformation[] | undefined;
private accountsRemovedPromise: Promise<boolean> | undefined;

// So that customers can easily share logs, try to only log PII using trace level
public constructor(private readonly logger?: vscode.LogOutputChannel) {
// Load accounts initially, then onDidChangeSessions can compare against them
void vscode.authentication.getAccounts(getConfiguredAuthProviderId()).then(accounts => {
this.priorAccounts = Array.from(accounts); // The Array.from is to get rid of the readonly marker on the array returned by the API
});
void this.accountsRemoved(); // Initialize priorAccounts
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public onDidSignIn(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable {
return this.onDidChangeSessions(true, callback, thisArg, disposables);
public dispose(): void {
this.listenerDisposable?.dispose();
this.onDidSignInEmitter.dispose();
this.onDidSignOutEmitter.dispose();
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
public onDidSignOut(callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable {
return this.onDidChangeSessions(false, callback, thisArg, disposables);
public onDidSignIn(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable {
this.listenIfNeeded();
return this.onDidSignInEmitter.event(callback, thisArg, disposables);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
private onDidChangeSessions(signIn: boolean, callback: () => any, thisArg?: any, disposables?: vscode.Disposable[]): vscode.Disposable {
const isASignInEvent = async () => {
const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId())); // The Array.from is to get rid of the readonly marker on the array returned by the API
const priorAccountCount = this.priorAccounts?.length ?? 0;
this.priorAccounts = currentAccounts;

// The only way a sign out happens is if an account is removed entirely from the list of accounts
if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
return false;
}

return true;
}
public onDidSignOut(callback: () => unknown, thisArg?: unknown, disposables?: vscode.Disposable[]): vscode.Disposable {
this.listenIfNeeded();
return this.onDidSignOutEmitter.event(callback, thisArg, disposables);
}

const wrappedCallback = () => {
const immediate = setImmediate(() => {
clearImmediate(immediate);
void callback.call(thisArg);
});
private listenIfNeeded(): void {
if (this.listenerDisposable) {
return;
}

const disposable = vscode.authentication.onDidChangeSessions(async e => {
this.listenerDisposable = vscode.authentication.onDidChangeSessions(async e => {
// Ignore any sign in that isn't for the configured auth provider
if (e.provider.id !== getConfiguredAuthProviderId()) {
return;
}

if (signIn) {
if (this.suppressSignInEvents || Date.now() < this.lastSignInEventFired + EventDebounce) {
return;
} else if (await isASignInEvent()) {
this.lastSignInEventFired = Date.now();
wrappedCallback();
}
if (this.suppressEvents || Date.now() < this.lastEventFired + EventDebounce) {
return;
}

this.lastEventFired = Date.now();

if (!await this.accountsRemoved()) {
this.logger?.debug('auth: Firing onDidSignIn event');
this.onDidSignInEmitter.fire();
} else {
if (Date.now() < this.lastSignOutEventFired + EventDebounce) {
return;
} else if (!await isASignInEvent()) {
this.lastSignOutEventFired = Date.now();
wrappedCallback();
}
this.logger?.debug('auth: Firing onDidSignOut event');
this.onDidSignOutEmitter.fire();
}
});

disposables?.push(disposable);
return disposable;
}

public dispose(): void {
// No-op, this class no longer has disposables
private async accountsRemoved(): Promise<boolean> {
// If there's already an ongoing accountsRemoved operation, return its result
if (this.accountsRemovedPromise) {
return this.accountsRemovedPromise;
}

// Create a new promise for this operation
this.accountsRemovedPromise = (async () => {
try {
this.suppressEvents = true;
const currentAccounts = Array.from(await vscode.authentication.getAccounts(getConfiguredAuthProviderId()));
const priorAccountCount = this.priorAccounts?.length ?? 0;
this.priorAccounts = currentAccounts;

// The only way a sign out happens is if an account is removed entirely from the list of accounts
if (currentAccounts.length === 0 || currentAccounts.length < priorAccountCount) {
return true;
}

return false;
} finally {
this.suppressEvents = false;
}
})();

try {
return await this.accountsRemovedPromise;
} finally {
// Clear the promise when done so future calls can proceed
this.accountsRemovedPromise = undefined;
}
}

/**
Expand Down Expand Up @@ -166,7 +179,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide
const allSubscriptions: AzureSubscription[] = [];
let accountCount: number; // only used for logging
try {
this.suppressSignInEvents = true;
this.suppressEvents = true;
// Get the list of tenants from each account (filtered or all)
const accounts = isGetSubscriptionsAccountFilter(filter) ? [filter.account] : await vscode.authentication.getAccounts(getConfiguredAuthProviderId());
accountCount = accounts.length;
Expand All @@ -186,7 +199,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide
allSubscriptions.push(...await this.getSubscriptionsForTenant(account));
}
} finally {
this.suppressSignInEvents = false;
this.suppressEvents = false;
}

// It's possible that by listing subscriptions in all tenants and the "home" tenant there could be duplicate subscriptions
Expand Down Expand Up @@ -262,7 +275,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide
public async signIn(tenantId?: string, account?: vscode.AuthenticationSessionAccountInformation): Promise<boolean> {
this.logger?.debug(`auth: Signing in (account="${account?.label ?? 'none'}") (tenantId="${tenantId ?? 'none'}")`);
try {
this.suppressSignInEvents = true;
this.suppressEvents = true;
const session = await getSessionFromVSCode([], tenantId, {
createIfNone: true,
// If no account is provided, then clear the session preference which tells VS Code to show the account picker
Expand All @@ -271,7 +284,7 @@ export class VSCodeAzureSubscriptionProvider implements AzureSubscriptionProvide
});
return !!session;
} finally {
this.suppressSignInEvents = false;
this.suppressEvents = false;
}
}

Expand Down
Loading