Skip to content

Commit eedbd53

Browse files
author
Zhan Li
committed
Add unit tests with HTTP mocking for vanilla dSTS + fix Authority parsing
Adds 6 HTTP-mocked unit tests for the vanilla dSTS (Dedicated Security Token Service) token-acquisition path in Microsoft.Identity.Web, and fixes MergedOptions.ParseAuthorityIfNecessary so that the natural / documented dSTS configuration form works end-to-end: options.Authority = "https://{host}/dstsv2/{tenantGuid}"; Without the fix, the AAD-style parser took the literal "dstsv2" as the tenant and dropped the actual tenant GUID, producing an authority MSAL rejected with "The DSTS authority URI should have at least 2 segments...". Tests cover: token endpoint URI; client_credentials grant body; second-call cache hit; OAuth2 error -> MsalServiceException mapping; SendX5C=true includes x5c JWT header; SendX5C=false omits it. All tests use the existing MockHttpClientFactory infrastructure (no real network / Key Vault / cert) and run in any CI environment. No public API changes.
1 parent ef54b6e commit eedbd53

2 files changed

Lines changed: 408 additions & 0 deletions

File tree

src/Microsoft.Identity.Web.TokenAcquisition/MergedOptions.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,43 @@ internal static void ParseAuthorityIfNecessary(MergedOptions mergedOptions, IdWe
504504
int indexVersion = authoritySpan.Slice(indexTenant + 1).IndexOf('/');
505505
int indexEndOfTenant = indexVersion == -1 ? authoritySpan.Length : indexVersion + indexTenant + 1;
506506

507+
// dSTS authorities have the shape https://{host}/dstsv2/{tenantGuid}, i.e. TWO
508+
// path segments rather than the AAD-style single segment. The literal "dstsv2"
509+
// is part of the instance and the SECOND path segment is the tenant.
510+
// Without this branch the generic AAD parser below would treat "dstsv2" as the
511+
// tenant and silently drop the actual tenant GUID, producing an authority that
512+
// MSAL rejects ("The DSTS authority URI should have at least 2 segments...").
513+
// This mirrors the existing B2C "/tfp/" special case in PrepareAuthorityInstanceForMsal.
514+
// Note: literal "dstsv2" is intentionally inlined rather than promoted to a
515+
// Constants entry to keep this fix internal-API-clean (no InternalAPI.txt churn
516+
// across all target frameworks). It only appears in this one parser branch.
517+
ReadOnlySpan<char> firstPathSegment = authoritySpan.Slice(indexTenant + 1, indexEndOfTenant - indexTenant - 1);
518+
if (firstPathSegment.Equals("dstsv2".AsSpan(), StringComparison.OrdinalIgnoreCase))
519+
{
520+
// indexEndOfTenant points at the '/' (or end-of-string) right after "dstsv2".
521+
// Tenant is the next path segment.
522+
int indexAfterDsts = indexEndOfTenant + 1;
523+
if (indexAfterDsts <= authoritySpan.Length)
524+
{
525+
int relIndexAfterTenant = indexAfterDsts >= authoritySpan.Length
526+
? -1
527+
: authoritySpan.Slice(indexAfterDsts).IndexOf('/');
528+
int indexEndOfDstsTenant = relIndexAfterTenant == -1
529+
? authoritySpan.Length
530+
: indexAfterDsts + relIndexAfterTenant;
531+
532+
mergedOptions.Instance = mergedOptions.PreserveAuthority
533+
? mergedOptions.Authority!
534+
: authoritySpan.Slice(0, indexEndOfTenant).ToString(); // "https://{host}/dstsv2"
535+
mergedOptions.TenantId = mergedOptions.PreserveAuthority
536+
? null
537+
: (indexAfterDsts < authoritySpan.Length
538+
? authoritySpan.Slice(indexAfterDsts, indexEndOfDstsTenant - indexAfterDsts).ToString()
539+
: string.Empty);
540+
return;
541+
}
542+
}
543+
507544
// In CIAM and B2C, customers will use "authority", not Instance and TenantId
508545
mergedOptions.Instance = mergedOptions.PreserveAuthority ? mergedOptions.Authority! : authoritySpan.Slice(0, indexTenant).ToString();
509546
mergedOptions.TenantId = mergedOptions.PreserveAuthority ? null : authoritySpan.Slice(indexTenant + 1, indexEndOfTenant - indexTenant - 1).ToString();

0 commit comments

Comments
 (0)