Skip to content

Throw on Authority vs Instance/TenantId conflict (OIDC + MSAL parity)#3873

Merged
iarekk merged 6 commits into
masterfrom
iarekk/authority-conflict-throw
Jun 22, 2026
Merged

Throw on Authority vs Instance/TenantId conflict (OIDC + MSAL parity)#3873
iarekk merged 6 commits into
masterfrom
iarekk/authority-conflict-throw

Conversation

@iarekk

@iarekk iarekk commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the silent EventId 500 warning with InvalidOperationException when both Authority and Instance/TenantId are explicitly configured. Both the OIDC sign-in path and the MSAL token-acquisition path now throw the same exception, resolving the behavioral divergence documented in the pinning tests (#3855).

Problem

Previously, configuring both Authority and Instance/TenantId produced different outcomes depending on the code path:

  • MSAL path (ParseAuthorityIfNecessary): logged a warning (EventId 500) and silently ignored Authority
  • OIDC path (OpenIdConnect middleware): silently honored Authority

Neither path surfaced the conflict to the developer at startup time.

Solution

Dual-latch provenance tracking on MergedOptions

Two latch properties track whether Authority and Instance/TenantId came from user config vs internal derivation:

  • AuthorityExplicitlyConfigured -- set when Authority is written from user config (MicrosoftIdentityOptions or JwtBearerOptions), NOT when backfilled by MicrosoftEntraApplicationOptions computed getter
  • InstanceOrTenantIdExplicitlyConfigured -- set when Instance or TenantId is written from user config (MicrosoftIdentityOptions or ConfidentialClientApplicationOptions), NOT when derived by ParseAuthorityIfNecessary or the computed Authority getter

Both latches use a set-only pattern (once true, never reverts to false).

Conflict detection

  • ThrowIfAuthorityConflict() helper on MergedOptions -- throws InvalidOperationException only when both latches are set and both sides are populated. Shared by MSAL and OIDC paths.
  • _authorityParsed flag -- makes ParseAuthorityIfNecessary idempotent (avoids false-positive on repeated calls after Authority is decomposed into Instance+TenantId)

Merge point coverage

The InstanceOrTenantIdExplicitlyConfigured latch is set at 4 user-config merge points:

  1. UpdateMergedOptionsFromMicrosoftIdentityOptions -- Instance
  2. UpdateMergedOptionsFromMicrosoftIdentityOptions -- TenantId
  3. UpdateMergedOptionsFromConfidentialClientApplicationOptions -- Instance
  4. UpdateMergedOptionsFromConfidentialClientApplicationOptions -- TenantId

Three synthetic merge points intentionally do NOT set the latch:

  1. ParseAuthorityIfNecessary (decomposes Authority into Instance+TenantId)
  2. UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions -- Instance (computed getter)
  3. UpdateMergedOptionsFromMicrosoftIdentityApplicationOptions -- TenantId (computed getter)

Test changes

  • Conflict pinning tests converted to Assert.Throws<InvalidOperationException>
  • Added SyntheticAuthority_NoThrow test (computed getter scenario = no conflict)
  • Conflict tests updated to set both latches (AuthorityExplicitlyConfigured + InstanceOrTenantIdExplicitlyConfigured)
  • Fixed 4 existing tests that incidentally created conflicts (redirect URI, regions, metadata address, CCA token-binding)

E2E validation

Manually tested with a learning app across all config scenarios:

  • Instance+TenantId only: app starts and works correctly
  • Authority only: app starts and works correctly (no false positive)
  • Authority + Instance + TenantId (conflict): throws InvalidOperationException at startup with clear message

Fixes #3869
Builds on #3855

Once this is merged, will improve the docs in #3617 and merge that one

@iarekk iarekk requested a review from a team as a code owner June 17, 2026 16:08
@iarekk iarekk force-pushed the iarekk/authority-conflict-throw branch 3 times, most recently from 5f54ccf to 51149d0 Compare June 18, 2026 10:09
iarekk and others added 6 commits June 22, 2026 10:59
…th MSAL conflict tests)

Add WebAppExtensionsAuthorityConflictTests, a complete counterpart to
MergedOptionsAuthorityConflictTests covering the OIDC sign-in code path
in MicrosoftIdentityWebAppAuthenticationBuilderExtensions when Authority
is configured alongside Instance, TenantId, Domain, or
SignUpSignInPolicyId.

The OIDC sign-in path and the MSAL token-acquisition path currently
exhibit different precedence and logging semantics:

  Token-acquisition (MergedOptions.ParseAuthorityIfNecessary):
    - Instance + TenantId WIN over Authority when both are configured.
    - Logs MergedOptionsLogging.AuthorityIgnored (EventId 500) on conflict.
    - Logs MergedOptionsLogging.AuthorityUsedConsiderInstanceTenantId
      (EventId 501) when Authority alone is used.

  OIDC sign-in (this class):
    - Authority WINS verbatim when set; Instance + TenantId only compose
      a fallback Authority via AuthorityHelpers.BuildAuthority when no
      Authority is configured.
    - Logs neither EventId 500 nor EventId 501.

The 17 tests added here pin each of these behaviors for AAD, B2C, and
CIAM scenarios, plus a robustness test for the no-logger-registered case:

  AAD precedence (5):
    OidcSignIn_AuthorityAndInstance_PinsAuthorityWins
    OidcSignIn_AuthorityAndTenantId_PinsAuthorityWins
    OidcSignIn_AuthorityAndInstanceAndTenantId_PinsAuthorityWins
    OidcSignIn_AuthorityOnly_PropagatesAuthorityAsIs                 (control)
    OidcSignIn_InstanceAndTenantIdOnly_ComposesAuthorityFromInstanceTenantId (control)

  B2C precedence (2):
    OidcSignIn_B2CAuthorityAndInstance_PinsAuthorityWins
    OidcSignIn_B2CInstanceAndDomain_ComposesAuthorityFromInstanceDomainUserFlow (control)

  CIAM precedence (2):
    OidcSignIn_CiamAuthorityAndInstance_PinsAuthorityWins
    OidcSignIn_CiamAuthorityOnly_PropagatesAuthorityThroughCiamHelper (control)

  Log assertions / bug evidence (7):
    OidcSignIn_AuthorityAndInstance_DoesNotLogAuthorityIgnoredWarning
    OidcSignIn_AuthorityAndTenantId_DoesNotLogAuthorityIgnoredWarning
    OidcSignIn_AuthorityAndInstanceAndTenantId_DoesNotLogAuthorityIgnoredWarning
    OidcSignIn_AuthorityOnly_DoesNotLogAuthorityUsedHint
    OidcSignIn_InstanceAndTenantIdOnly_DoesNotLogAnyAuthorityWarning
    OidcSignIn_B2CAuthorityAndInstance_DoesNotLogAuthorityIgnoredWarning
    OidcSignIn_CiamAuthorityAndInstance_DoesNotLogAuthorityIgnoredWarning

  Robustness (1):
    OidcSignIn_AuthorityAndInstance_DoesNotThrowWhenNoLoggerRegistered

These are PINNING tests: they document what the code does today, not
what it should do. They will fail intentionally if the OIDC sign-in
path is changed to mirror the token-acquisition path -- that failure is
the signal a behavior fix has landed. Update assertions deliberately
when that happens and preserve the rationale in test names and comments.

Test infrastructure:
  - BuildAndGetOidcOptions: per-test helper that wires up
    AddMicrosoftIdentityWebApp with a synthetic IConfigurationSection
    containing only the keys specified by the caller, builds the
    ServiceProvider, and resolves OpenIdConnectOptions.
  - TestLogger / TestLoggerProvider: ILoggerProvider that aggregates
    every category-specific ILogger call into a single Entries list,
    enabling DoesNotContain assertions over (LogLevel, EventId.Id, Message).

Verified: dotnet test on net10.0 -- 17/17 pass.
Regression check across WebAppExtensionsTests, MergedOptionsAuthorityConflictTests,
PopulateOpenIdOptionsFromMergedOptionsTests, AuthorityHelpersTests,
MergedOptionsAuthorityParsingTests, CiamAuthorityHelperTest, and this
new class: 173/173 pass.

Local repro notes (unrelated to this commit):
  - Microsoft.Identity.Web.Certificateless/AzureIdentityForKubernetesClientAssertion.cs:77
    has a pre-existing CS8604 caught by the .NET 10 SDK that
    Directory.Build.props' TreatWarningsAsErrors=true promotes to an error.
    Workaround: -p:TreatWarningsAsErrors=false until that nullability
    bug is fixed separately.
  - The repo's UI / TokenAcquisition projects need -p:TargetNetNext=True
    to include the net10.0 TFM
    (see Microsoft.Identity.Web.UI.csproj line 9 and similar).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Replace the silent EventId 500 warning with InvalidOperationException when
the user explicitly configures both Authority and Instance/TenantId.

Implementation:
- Add AuthorityExplicitlyConfigured latch to MergedOptions (set when
  Authority is written from MicrosoftIdentityOptions or JwtBearerOptions,
  NOT set when backfilled by MicrosoftEntraApplicationOptions getter)
- Add _authorityParsed flag to make ParseAuthorityIfNecessary idempotent
  (avoids false-positive on second call after Authority is parsed into
  Instance+TenantId)
- Throw in ParseAuthorityIfNecessary (MSAL path) when latch is set and
  both Authority + Instance/TenantId are present
- Throw in OIDC sign-in path (MicrosoftIdentityWebAppAuthenticationBuilder
  Extensions) with same check, before Authority is consumed

Test updates:
- Convert conflict pinning tests to Assert.Throws<InvalidOperationException>
- Add SyntheticAuthority_NoThrow test for computed-getter scenario
- Fix existing tests that incidentally created conflicts (redirect URI,
  regions, metadata address tests)

Fixes #3869

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Deduplicates the conflict check + throw from ParseAuthorityIfNecessary
and the OIDC extensions into a single instance method.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ty+Instance

Same pattern as redirect URI and regions tests: remove Instance from
ConfidentialClientApplicationOptions since these tests verify cache-key
isolation, not authority resolution.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…itive throws

When only Authority is configured, the library internally derives Instance/TenantId
(via ParseAuthorityIfNecessary and computed getters). The single-latch design
incorrectly threw on this valid scenario. The dual-latch approach requires both
Authority AND Instance/TenantId to come from user config before throwing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Exercise the UpdateMergedOptions* pipeline (MicrosoftIdentityOptions and
ConfidentialClientApplicationOptions) so the AuthorityExplicitlyConfigured
/ InstanceOrTenantIdExplicitlyConfigured latches are set through the real
merge code path, not manually.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@iarekk iarekk force-pushed the iarekk/authority-conflict-throw branch from 3a0c7aa to db0f75e Compare June 22, 2026 09:59
@iarekk iarekk merged commit 9c772f3 into master Jun 22, 2026
4 checks passed
@iarekk iarekk deleted the iarekk/authority-conflict-throw branch June 22, 2026 10:18
iarekk added a commit that referenced this pull request Jun 22, 2026
Reflect PR #3873 behavior change -- mixing Authority with
Instance/TenantId now throws InvalidOperationException at startup
instead of silently ignoring Authority with a warning.

Updated across all doc files:
- Decision tree, precedence table, error section (authority-configuration.md)
- Common mistakes sections (b2c, ciam examples)
- FAQ questions about conflicts and upgrades
- Migration guide: warning -> exception language
- README: removed PreserveAuthority from link text
- Changelog: updated entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
iarekk added a commit that referenced this pull request Jun 22, 2026
Reflect PR #3873 behavior change -- mixing Authority with
Instance/TenantId now throws InvalidOperationException at startup
instead of silently ignoring Authority with a warning.

Updated across all doc files:
- Decision tree, precedence table, error section (authority-configuration.md)
- Common mistakes sections (b2c, ciam examples)
- FAQ questions about conflicts and upgrades
- Migration guide: warning -> exception language
- README: removed PreserveAuthority from link text
- Changelog: updated entry

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
iarekk added a commit that referenced this pull request Jun 22, 2026
… parity) (#3873)" (#3888)

This reverts commit 9c772f3.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
iarekk added a commit that referenced this pull request Jun 23, 2026
Re-add the changes introduced in #3873 and reverted in #3888.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
iarekk added a commit that referenced this pull request Jun 24, 2026
Re-add the changes introduced in #3873 and reverted in #3888.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
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.

ASP.NET middleware and MSAL token acquisition resolve the authority precedence differently

2 participants