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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Identity.Client;
using Abstractions = Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web
{
/// <summary>
/// Builds <see cref="Abstractions.AcquireTokenResult"/> instances from MSAL
/// <see cref="AuthenticationResult"/>s, including token-acquisition metadata.
/// </summary>
internal static class AcquireTokenResultFactory
{
public static Abstractions.AcquireTokenResult FromMsal(AuthenticationResult result) =>
new(
result.AccessToken,
result.ExpiresOn,
result.TenantId,
result.IdToken,
result.Scopes,
result.CorrelationId,
result.TokenType)
{
AdditionalResponseParameters = result.AdditionalResponseParameters,
BindingCertificate = result.BindingCertificate,
Metadata = MapMetadata(result),
};

private static Abstractions.TokenAcquisitionMetadata? MapMetadata(AuthenticationResult result)
{
AuthenticationResultMetadata? source = result.AuthenticationResultMetadata;
if (source is null)
{
return null;
}

// Enum casts are intentional: Abstractions enum values are kept in lock-step with MSAL.
// Any future MSAL-only enum member falls through to its numeric value, which is
// forward-compatible for diagnostic consumers.
return new Abstractions.TokenAcquisitionMetadata
{
TokenSource = (Abstractions.AcquiredTokenSource)source.TokenSource,
CacheRefreshReason = (Abstractions.AcquiredTokenCacheRefreshReason)source.CacheRefreshReason,
CacheLevel = (Abstractions.AcquiredTokenCacheLevel)source.CacheLevel,
TokenEndpoint = source.TokenEndpoint,
DurationTotalInMs = source.DurationTotalInMs,
DurationInHttpInMs = source.DurationInHttpInMs,
DurationInCacheInMs = source.DurationInCacheInMs,
RefreshOn = source.RefreshOn,
RegionDetails = MapRegionDetails(source.RegionDetails),
};
}

private static Abstractions.AcquiredTokenRegionDetails? MapRegionDetails(RegionDetails? source) =>
source is null ? null : new Abstractions.AcquiredTokenRegionDetails
{
RegionUsed = source.RegionUsed,
RegionOutcome = (Abstractions.AcquiredTokenRegionOutcome)source.RegionOutcome,
AutoDetectionError = source.AutoDetectionError,
};
}
}
21 changes: 2 additions & 19 deletions src/Microsoft.Identity.Web.TokenAcquisition/TokenAcquirer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,14 +45,7 @@ async Task<AcquireTokenResult> ITokenAcquirer.GetTokenForUserAsync(
tokenAcquisitionOptions.LongRunningWebApiSessionKey = effectiveOptions.LongRunningWebApiSessionKey;
}

return new AcquireTokenResult(
result.AccessToken,
result.ExpiresOn,
result.TenantId,
result.IdToken,
result.Scopes,
result.CorrelationId,
result.TokenType);
return AcquireTokenResultFactory.FromMsal(result);
}

async Task<AcquireTokenResult> ITokenAcquirer.GetTokenForAppAsync(string scope, AcquireTokenOptions? tokenAcquisitionOptions, CancellationToken cancellationToken)
Expand All @@ -66,17 +59,7 @@ async Task<AcquireTokenResult> ITokenAcquirer.GetTokenForAppAsync(string scope,
GetEffectiveTokenAcquisitionOptions(tokenAcquisitionOptions, authenticationScheme, cancellationToken)
).ConfigureAwait(false);

return new AcquireTokenResult(
result.AccessToken,
result.ExpiresOn,
result.TenantId,
result.IdToken,
result.Scopes,
result.CorrelationId,
result.TokenType)
{
BindingCertificate = result.BindingCertificate
};
return AcquireTokenResultFactory.FromMsal(result);
}

private static TokenAcquisitionOptions? GetEffectiveTokenAcquisitionOptions(AcquireTokenOptions? tokenAcquisitionOptions, string? authenticationScheme, CancellationToken cancellationToken)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,14 +205,7 @@ private async Task<AcquireTokenResult> AddAccountToCacheFromAuthorizationCodeInt
true,
null);

return new AcquireTokenResult(
result.AccessToken,
result.ExpiresOn,
result.TenantId,
result.IdToken,
result.Scopes,
result.CorrelationId,
result.TokenType);
return AcquireTokenResultFactory.FromMsal(result);
}
catch (MsalServiceException exMsal) when (retryCount < MaxCertificateRetries && IsInvalidClientCertificateOrSignedAssertionError(exMsal))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Cache;
using Microsoft.Identity.Client.Region;
using Xunit;
using Abstractions = Microsoft.Identity.Abstractions;

namespace Microsoft.Identity.Web.Test
{
/// <summary>
/// Pins the numeric lockstep between MSAL enums and their Abstractions counterparts.
/// <see cref="AcquireTokenResultFactory.MapMetadata"/> casts MSAL enum values directly
/// to Abstractions enum values. If MSAL ever renumbers a member or adds a new one
/// out of order, these tests will fail loudly instead of producing silently-wrong
/// telemetry. One assertion per member of every casted enum.
/// </summary>
public class AcquireTokenResultFactoryEnumRoundTripTests
{
[Theory]
[InlineData(TokenSource.IdentityProvider, Abstractions.AcquiredTokenSource.IdentityProvider)]
[InlineData(TokenSource.Cache, Abstractions.AcquiredTokenSource.Cache)]
[InlineData(TokenSource.Broker, Abstractions.AcquiredTokenSource.Broker)]
public void TokenSource_NumericCast_RoundTrips(TokenSource msal, Abstractions.AcquiredTokenSource expected)
{
Assert.Equal((int)expected, (int)msal);
Assert.Equal(expected, (Abstractions.AcquiredTokenSource)msal);
}

[Theory]
[InlineData(CacheRefreshReason.NotApplicable, Abstractions.AcquiredTokenCacheRefreshReason.NotApplicable)]
[InlineData(CacheRefreshReason.ForceRefreshOrClaims, Abstractions.AcquiredTokenCacheRefreshReason.ForceRefreshOrClaims)]
[InlineData(CacheRefreshReason.NoCachedAccessToken, Abstractions.AcquiredTokenCacheRefreshReason.NoCachedAccessToken)]
[InlineData(CacheRefreshReason.Expired, Abstractions.AcquiredTokenCacheRefreshReason.Expired)]
[InlineData(CacheRefreshReason.ProactivelyRefreshed, Abstractions.AcquiredTokenCacheRefreshReason.ProactivelyRefreshed)]
[InlineData(CacheRefreshReason.CacheDisabled, Abstractions.AcquiredTokenCacheRefreshReason.CacheDisabled)]
public void CacheRefreshReason_NumericCast_RoundTrips(CacheRefreshReason msal, Abstractions.AcquiredTokenCacheRefreshReason expected)
{
Assert.Equal((int)expected, (int)msal);
Assert.Equal(expected, (Abstractions.AcquiredTokenCacheRefreshReason)msal);
}

[Theory]
[InlineData(CacheLevel.None, Abstractions.AcquiredTokenCacheLevel.None)]
[InlineData(CacheLevel.Unknown, Abstractions.AcquiredTokenCacheLevel.Unknown)]
[InlineData(CacheLevel.L1Cache, Abstractions.AcquiredTokenCacheLevel.L1Cache)]
[InlineData(CacheLevel.L2Cache, Abstractions.AcquiredTokenCacheLevel.L2Cache)]
public void CacheLevel_NumericCast_RoundTrips(CacheLevel msal, Abstractions.AcquiredTokenCacheLevel expected)
{
Assert.Equal((int)expected, (int)msal);
Assert.Equal(expected, (Abstractions.AcquiredTokenCacheLevel)msal);
}

[Theory]
[InlineData(RegionOutcome.None, Abstractions.AcquiredTokenRegionOutcome.None)]
[InlineData(RegionOutcome.UserProvidedValid, Abstractions.AcquiredTokenRegionOutcome.UserProvidedValid)]
[InlineData(RegionOutcome.UserProvidedAutodetectionFailed, Abstractions.AcquiredTokenRegionOutcome.UserProvidedAutodetectionFailed)]
[InlineData(RegionOutcome.UserProvidedInvalid, Abstractions.AcquiredTokenRegionOutcome.UserProvidedInvalid)]
[InlineData(RegionOutcome.AutodetectSuccess, Abstractions.AcquiredTokenRegionOutcome.AutodetectSuccess)]
[InlineData(RegionOutcome.FallbackToGlobal, Abstractions.AcquiredTokenRegionOutcome.FallbackToGlobal)]
public void RegionOutcome_NumericCast_RoundTrips(RegionOutcome msal, Abstractions.AcquiredTokenRegionOutcome expected)
{
Assert.Equal((int)expected, (int)msal);
Assert.Equal(expected, (Abstractions.AcquiredTokenRegionOutcome)msal);
}

/// <summary>
/// Defends against MSAL adding a new member at any position other than the next sequential
/// integer. If MSAL grows or the Abstractions counterpart grows, both sides must be updated
/// together. A drift here means the casts in AcquireTokenResultFactory will silently produce
/// wrong values for new members.
/// </summary>
[Fact]
public void EnumMemberCounts_MatchAbstractions()
{
Assert.Equal(
System.Enum.GetValues(typeof(TokenSource)).Length,
System.Enum.GetValues(typeof(Abstractions.AcquiredTokenSource)).Length);

Assert.Equal(
System.Enum.GetValues(typeof(CacheRefreshReason)).Length,
System.Enum.GetValues(typeof(Abstractions.AcquiredTokenCacheRefreshReason)).Length);

Assert.Equal(
System.Enum.GetValues(typeof(CacheLevel)).Length,
System.Enum.GetValues(typeof(Abstractions.AcquiredTokenCacheLevel)).Length);

Assert.Equal(
System.Enum.GetValues(typeof(RegionOutcome)).Length,
System.Enum.GetValues(typeof(Abstractions.AcquiredTokenRegionOutcome)).Length);
}
}
}
Loading