Skip to content

Add manual tenant sign-in fallback for Conditional Access tenants#1511

Open
CoffeeKanzler wants to merge 10 commits into
microsoft:mainfrom
CoffeeKanzler:issue-1332-azure-tenant-setting
Open

Add manual tenant sign-in fallback for Conditional Access tenants#1511
CoffeeKanzler wants to merge 10 commits into
microsoft:mainfrom
CoffeeKanzler:issue-1332-azure-tenant-setting

Conversation

@CoffeeKanzler

@CoffeeKanzler CoffeeKanzler commented Jun 23, 2026

Copy link
Copy Markdown

No description provided.

@CoffeeKanzler CoffeeKanzler requested a review from a team as a code owner June 23, 2026 10:01
Copilot AI review requested due to automatic review settings June 23, 2026 10:01
@CoffeeKanzler

Copy link
Copy Markdown
Author

@microsoft-github-policy-service agree

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds support for the existing azure.tenant setting when authenticating via VS Code’s built-in Microsoft authentication provider, enabling tenant-directed sign-in and tenant-scoped session requests (notably for auth-record export) consistent with az login --tenant ....

Changes:

  • Introduces src/utils/azureTenantSetting.ts to normalize/read azure.tenant, construct VSCODE_TENANT:<tenant> scopes, and provide tenant fallback selection logic.
  • Passes configured tenant through sign-in flows (login command + subscription provider context) and applies tenant-scoped auth session requests during auth-record export.
  • Adds configuration contribution/localization for azure.tenant and adds unit tests validating tenant normalization/scope behavior and logIn tenant forwarding.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/azureTenantSetting.test.ts Adds tests for tenant normalization, tenant scope construction, tenant fallback logic, and tenant forwarding in logIn.
src/utils/azureTenantSetting.ts Implements helpers for reading/normalizing azure.tenant, appending VSCODE_TENANT scope, and choosing effective tenant ID for auth.
src/services/VSCodeAzureSubscriptionProvider.ts Adds tenant-aware signIn override and tenant fallback in subscription context generation.
src/exportAuthRecord.ts Applies tenant-scoped session request for export and uses configured tenant in tenant resolution logic.
src/commands/accounts/logIn.ts Passes configured azure.tenant into provider signIn.
package.nls.json Adds localized description string for azure.tenant.
package.json Contributes the azure.tenant setting to VS Code Settings UI.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/exportAuthRecord.ts Outdated
Comment thread src/services/VSCodeAzureSubscriptionProvider.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread src/exportAuthRecord.ts Outdated
Comment on lines 267 to 269
export function getTenantId(session: unknown, context?: IActionContext): string | undefined {
return extractTenantIdFromIdToken(session, context) ?? getConfiguredAzureTenant();
}

@bwateratmsft bwateratmsft left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The Azure Resources extension supports multi-tenant authentication on purpose. You can filter out tenants you aren't interested in looking at with the Accounts and Tenants view and the subscription filter. I don't see why this is necessary.

@CoffeeKanzler

Copy link
Copy Markdown
Author

You can not login into some tenants that enforce that via conditional login. You NEED to specifiy the tenant manually to start the Auth workflow via the Microsoft Authenticator

@bwateratmsft

bwateratmsft commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

Does the "Sign in to Tenant (Directory)..." command work?

@CoffeeKanzler

Copy link
Copy Markdown
Author

No sadly it does not. The flow from my understanding is:

Azure: Sign In
-> provider.signIn(undefined)
-> authentication.getSession("microsoft", ARM scopes)
-> no VSCODE_TENANT scope
-> Microsoft Authentication defaults to the "organizations" authority

Sign in to Tenant
-> getAccounts()
-> for each already-authenticated account: getTenantsForAccount()
-> call Azure listTenants()
-> populate Quick Pick with returned tenants
-> user selects one of those discovered tenants
-> provider.signIn(selectedTenant)
-> VSCODE_TENANT: is finally added

getAccounts() requires an account already registered with the VS Code Microsoft authentication provider. If the initial generic authentication failed, there is no account to use for tenant discovery.

This creates a circular dependency:

Tenant-specific authentication is required to satisfy Conditional Access and sign into tenant requires generic authentication and tenant discovery before it can request tenant-specific authentication

@bwateratmsft

Copy link
Copy Markdown
Contributor

That makes sense. Perhaps an input box in Sign in to Tenant (Directory)... to ask for tenant ID, or something like that would work? If the default sign in fails?

(side note, I wish Azure auth wasn't so nightmarishly overcomplicated...)

@CoffeeKanzler

Copy link
Copy Markdown
Author

Yeah that sounds like a better Idea than my current solution. I can look into that and rewrite this.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Comment thread src/commands/accounts/signInToTenant.ts Outdated
Comment on lines +72 to +76
validateInput: value => value?.trim() ? undefined : localize('enterTenantIdValidation', 'Enter a tenant ID or domain.'),
});

return tenantId ? { tenantId: tenantId.trim() } as TenantIdAndAccount : undefined;
}
Comment thread src/commands/accounts/signInToTenant.ts Outdated
return unauthenticatedTenants
.sort((a, b) => (a.displayName ?? '').localeCompare(b.displayName ?? ''))
.map(tenant => ({
label: tenant.displayName ?? '',
Comment thread test/signInToTenant.test.ts Outdated
signIn: (tenant?: TenantIdAndAccount) => Promise<boolean>;
}): AzureSubscriptionProvider {
return {
onRefreshSuggested: (() => ({ dispose: () => { /* no-op */ } })) as unknown as vscode.Event<RefreshSuggestedEvent>,
Comment thread src/utils/manualSignInTenant.ts Outdated
Comment on lines +24 to +28
export async function setManualSignInTenant(tenant: string): Promise<void> {
if (!ext.context) {
testingManualSignInTenant = normalizeTenant(tenant);
return;
}
@CoffeeKanzler

Copy link
Copy Markdown
Author

I'll rework the description and work on the findings later this week probably. For now my tests are successful but I'll have to work in the additions of Copilot

@CoffeeKanzler CoffeeKanzler changed the title Support azure.tenant for Microsoft authentication sign-in Add manual tenant sign-in fallback for Conditional Access tenants Jun 23, 2026
@CoffeeKanzler CoffeeKanzler requested a review from Copilot June 23, 2026 17:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.

Comment thread src/utils/manualSignInTenant.ts Outdated
Comment on lines +1 to +4
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
Comment thread src/commands/accounts/signInToTenant.ts Outdated
Comment on lines +64 to +67
return unauthenticatedTenants
.sort((a, b) => (a.displayName ?? '').localeCompare(b.displayName ?? ''))
.map(tenant => ({
label: tenant.displayName ?? tenant.defaultDomain ?? tenant.tenantId,
Comment thread src/commands/accounts/signInToTenant.ts Outdated
Comment on lines +33 to +41
try {
const picks = await getPicks(subscriptionProvider, account);
if (picks.length) {
const pick = await context.ui.showQuickPick(picks, {
placeHolder: localize('selectTenantPlaceholder', 'Select a Tenant (Directory) to Sign In To'),
matchOnDescription: true,
});
return { tenant: pick.data, isManual: false };
}
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
- Sort the tenant quick pick by the same display/domain/id fallback used
  for the label, so tenants without a display name no longer clump at the top.
- Add a test for the manual-entry fallback when discovery returns no tenants
  (account exists but has no unauthenticated tenants).
@CoffeeKanzler

Copy link
Copy Markdown
Author

@bwateratmsft One open design question I'd like your call on before this is final:

When a user manually enters a tenant and sign-in succeeds, we persist it (globalState) and use it as the account-level authentication fallback — i.e. it's injected as the tenantId only for contexts that don't already specify one (the "list tenants for account" path). Contexts that already have a tenant ID ignore it, and ARM's tenant-list endpoint is account-wide, so it doesn't hide other tenants from the UI.

The thing I'm unsure about is its lifecycle: it's currently never cleared. The one case that can actually wedge things is a stale/revoked manual tenant — if the user later loses access to it, account-level enumeration keeps trying to get a token for it. Clearing it on a successful discovered sign-in doesn't help there (you can't reach discovery if enumeration is already blocked).

Options I see: (a) leave it as-is, (b) add an explicit "Clear manual tenant" affordance, or (c) self-heal by dropping it if its silent token acquisition fails. Do you have a preference, or would you rather keep this PR to the bootstrap fix and handle lifecycle separately?

@bwateratmsft bwateratmsft left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Once an account is signed in (to a specific tenant), does it still hit the endpoint that CA is blocking? The MSAL broker might be "sticky" enough to remember the tenant for us once the account has been signed in, removing the need to persist the tenant ID ourselves. Not certain, but worth trying.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We should make the changes in the auth package in microsoft/vscode-azuretools rather than duplicating the code there. Potentially we add a tenant or tenant ID arg to that function? @alexweininger thoughts?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@CoffeeKanzler thanks for opening the PR!

getPicks here duplicates the auth package's existing picker, can we instead just delegate to the auth package's signInToTenant(provider) and only fall back to the manual tenant input box when it throws NotSignedInError? That avoids the duplication and any auth-package changes. Also, do we actually need the manualSignInTenant persistence + getSubscriptionContext override?

For example: main...alexweininger/sign-in-to-tenant-manual-entry

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Happy to refactor the picker to delegate to the auth package's signInToTenant(provider) with an input-box fallback on NotSignedInError, @alexweininger.

On whether we still need the manualSignInTenant persistence + getSubscriptionContext override: I tested exactly this. The manual sign-in itself succeeds, but resources never populate afterward.

The reason: subscription enumeration goes through getSubscriptionContext({ account, tenantId: undefined }) → listTenants(), and with tenantId undefined the silent token request hits the common/organizations
endpoint. So no subscriptions come back and the tree stays empty. The override re-injects the signed-in tenant into that account-level context, which scopes the silent token correctly and makes resources load.

It has to be persisted (not just in-memory) because the same enumeration re-runs on every reload.

@bwateratmsft — to your MSAL-stickiness question, that's what I was hoping too, but in practice the broker is only sticky for the interactive sign-in, not for the silent enumeration token. Without the persisted
tenant, a reload gives an empty tree.

So my proposal: Refactor the delegation (drop the duplicated picker) and keep the tenant persistence + context override (necessary for resources to actually load).

If you'd rather that re-scoping live in the auth package instead like e.g. a "default/sticky tenant" the provider applies to account-level requests then I'd need to further study the auth package after I am back.

Heads up on timing: I'm traveling in about an hour and won't be back until Friday at the earliest, so apologies in advance for the slow follow-up. I'll pick this up as soon as I'm back.

@alexweininger alexweininger Jun 24, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry to go back and forth on it, but now that we know we need to persist that value, then I think it's best kept as a setting like your original design.

  1. Drop the manualSignInTenant globalState, use an advanced setting instead.
  2. Move the discovery fix to getTenantsForAccount (we already override it, and it gets the account): when ARM /tenants returns empty/throws for a conditional-access user, inject AzureTenant { tenantId: <setting>, account }. The normal per-tenant flow then lists subscriptions via that tenant's session, so we can drop the getSubscriptionContext override entirely.
  3. Keep using the setting for the initial signIn({ tenantId }).

summary: one setting + the existing public override, no globalState, no internal coupling. Clients consume our discovered subscriptions via the Resources API, so this covers the whole extension family too.

Address review feedback from microsoft#1511:

- Delegate the tenant picker to the auth package's signInToTenant and
  fall back to a tenant ID/domain input box only on NotSignedInError.
- Drop the manualSignInTenant globalState; persist the manually entered
  tenant in a new azure.tenant setting (machine scope) instead.
- Move the conditional-access discovery fix into getTenantsForAccount:
  when tenant discovery throws or returns no tenants, inject a synthetic
  AzureTenant from the setting so the normal per-tenant subscription
  flow runs against the tenant-scoped session. This removes the
  getSubscriptionContext override entirely.

Claude-Session: https://claude.ai/code/session_01Cqc27MN7o36f4avwMAyj3M
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants