diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs index 1aac3dde84..438174e73b 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ApplicationConfiguration.cs @@ -159,7 +159,6 @@ public X509Certificate2 ClientCredentialCertificate return null; } } - #endregion #region Region diff --git a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs index 8c5e702d33..3576807381 100644 --- a/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs +++ b/src/client/Microsoft.Identity.Client/AppConfig/ConfidentialClientApplicationBuilder.cs @@ -341,8 +341,6 @@ public ConfidentialClientApplicationBuilder WithGenericAuthority(string authorit /// The builder to chain the .With methods public ConfidentialClientApplicationBuilder WithTelemetryClient(params ITelemetryClient[] telemetryClients) { - ValidateUseOfExperimentalFeature("ITelemetryClient"); - if (telemetryClients == null) { throw new ArgumentNullException(nameof(telemetryClients)); diff --git a/src/client/Microsoft.Identity.Client/Cache/CacheLevel.cs b/src/client/Microsoft.Identity.Client/Cache/CacheLevel.cs new file mode 100644 index 0000000000..9ec8cda0e9 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/Cache/CacheLevel.cs @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Microsoft.Identity.Client.Cache +{ + /// + /// Identifies the type of cache that the token was read from. Cache implementations must provide this. + /// + public enum CacheLevel + { + /// + /// Specifies that the cache level used is None. + /// Token was retrieved from ESTS + /// + None = 0, + /// + /// Specifies that the cache level used is unknown. + /// Token was retrieved from cache but the token cache implementation didn't specify which cache level was used. + /// + Unknown = 1, + /// + /// Specifies if the L1 cache is used. + /// + L1Cache = 2, + /// + /// Specifies if the L2 cache is used. + L2Cache = 3 + } +} diff --git a/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs b/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs index 6489109bab..5533bfb2bc 100644 --- a/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs +++ b/src/client/Microsoft.Identity.Client/Cache/CacheSessionManager.cs @@ -10,6 +10,7 @@ using Microsoft.Identity.Client.Internal; using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; namespace Microsoft.Identity.Client.Cache { @@ -106,6 +107,7 @@ private async Task RefreshCacheForReadOperationsAsync() await TokenCacheInternal.Semaphore.WaitAsync(_requestParams.RequestContext.UserCancellationToken).ConfigureAwait(false); _requestParams.RequestContext.Logger.Verbose(()=>"[Cache Session Manager] Entered cache semaphore"); + TelemetryData telemetryData = new TelemetryData(); Stopwatch stopwatch = new Stopwatch(); try { @@ -129,7 +131,8 @@ private async Task RefreshCacheForReadOperationsAsync() requestScopes: _requestParams.Scope, requestTenantId: _requestParams.AuthorityManager.OriginalAuthority.TenantId, identityLogger: _requestParams.RequestContext.Logger.IdentityLogger, - piiLoggingEnabled: _requestParams.RequestContext.Logger.PiiLoggingEnabled); + piiLoggingEnabled: _requestParams.RequestContext.Logger.PiiLoggingEnabled, + telemetryData: telemetryData); stopwatch.Start(); await TokenCacheInternal.OnBeforeAccessAsync(args).ConfigureAwait(false); @@ -155,7 +158,8 @@ private async Task RefreshCacheForReadOperationsAsync() requestScopes: _requestParams.Scope, requestTenantId: _requestParams.AuthorityManager.OriginalAuthority.TenantId, identityLogger: _requestParams.RequestContext.Logger.IdentityLogger, - piiLoggingEnabled: _requestParams.RequestContext.Logger.PiiLoggingEnabled); + piiLoggingEnabled: _requestParams.RequestContext.Logger.PiiLoggingEnabled, + telemetryData: telemetryData); await TokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); RequestContext.ApiEvent.DurationInCacheInMs += stopwatch.ElapsedMilliseconds; @@ -170,6 +174,7 @@ private async Task RefreshCacheForReadOperationsAsync() { TokenCacheInternal.Semaphore.Release(); _requestParams.RequestContext.Logger.Verbose(()=>"[Cache Session Manager] Released cache semaphore"); + RequestContext.ApiEvent.CacheLevel = telemetryData.CacheLevel; } } } diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs index 966172bbba..ed8cdbaad6 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/CertificateAndClaimsClientCredential.cs @@ -8,6 +8,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.Internal.ClientCredential @@ -19,6 +20,8 @@ internal class CertificateAndClaimsClientCredential : IClientCredential private readonly string _base64EncodedThumbprint; // x5t public X509Certificate2 Certificate { get; } + public AssertionType AssertionType => AssertionType.CertificateWithoutSni; + public CertificateAndClaimsClientCredential(X509Certificate2 certificate, IDictionary claimsToSign, bool appendDefaultClaims) { Certificate = certificate; @@ -42,7 +45,7 @@ public Task AddConfidentialClientParametersAsync( tokenEndpoint, _claimsToSign, _appendDefaultClaims); - + string assertion = jwtToken.Sign(Certificate, _base64EncodedThumbprint, sendX5C); oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer); diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs index 87b1a24184..fed446b39a 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/IClientCredential.cs @@ -11,12 +11,15 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.Internal.ClientCredential { internal interface IClientCredential { + AssertionType AssertionType { get; } + Task AddConfidentialClientParametersAsync( OAuth2Client oAuth2Client, ILoggerAdapter logger, diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs index 0747441d88..7c707d0389 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SecretStringClientCredential.cs @@ -6,6 +6,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.Internal.ClientCredential @@ -14,6 +15,8 @@ internal class SecretStringClientCredential : IClientCredential { internal string Secret { get; } + public AssertionType AssertionType => AssertionType.Secret; + public SecretStringClientCredential(string secret) { Secret = secret; diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs index 9cb133572d..b63209aa26 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionClientCredential.cs @@ -6,6 +6,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client.Internal.ClientCredential @@ -14,10 +15,13 @@ internal class SignedAssertionClientCredential : IClientCredential { private readonly string _signedAssertion; + public AssertionType AssertionType => AssertionType.ClientAssertion; + public SignedAssertionClientCredential(string signedAssertion) { _signedAssertion = signedAssertion; } + public Task AddConfidentialClientParametersAsync(OAuth2Client oAuth2Client, ILoggerAdapter logger, ICryptographyManager cryptographyManager, string clientId, string tokenEndpoint, bool sendX5C, CancellationToken cancellationToken) { oAuth2Client.AddBodyParameter(OAuth2Parameter.ClientAssertionType, OAuth2AssertionType.JwtBearer); diff --git a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs index 8618dac916..1f7aeed7a3 100644 --- a/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs +++ b/src/client/Microsoft.Identity.Client/Internal/ClientCredential/SignedAssertionDelegateClientCredential.cs @@ -7,6 +7,7 @@ using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.PlatformsCommon.Interfaces; +using Microsoft.Identity.Client.TelemetryCore; namespace Microsoft.Identity.Client.Internal.ClientCredential { @@ -14,6 +15,7 @@ internal class SignedAssertionDelegateClientCredential : IClientCredential { internal Func> _signedAssertionDelegate { get; } internal Func> _signedAssertionWithInfoDelegate { get; } + public AssertionType AssertionType => AssertionType.ClientAssertion; [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public SignedAssertionDelegateClientCredential(Func> signedAssertionDelegate) diff --git a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs index d2fd26f748..ca535075eb 100644 --- a/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs +++ b/src/client/Microsoft.Identity.Client/Internal/RequestContext.cs @@ -2,11 +2,13 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Threading; using Microsoft.Identity.Client.Core; using Microsoft.Identity.Client.Internal.Logger; using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; using Microsoft.IdentityModel.Abstractions; namespace Microsoft.Identity.Client.Internal diff --git a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs index 18ee59ccd0..df1a0c1d24 100644 --- a/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs +++ b/src/client/Microsoft.Identity.Client/Internal/Requests/RequestBase.cs @@ -104,13 +104,14 @@ public async Task RunAsync(CancellationToken cancellationT { apiEvent.ApiErrorCode = ex.ErrorCode; AuthenticationRequestParameters.RequestContext.Logger.ErrorPii(ex); - LogErrorTelemetryToClient(ex.ErrorCode, telemetryEventDetails, telemetryClients); + LogMsalErrorTelemetryToClient(ex, telemetryEventDetails, telemetryClients); throw; } catch (Exception ex) { apiEvent.ApiErrorCode = ex.GetType().Name; AuthenticationRequestParameters.RequestContext.Logger.ErrorPii(ex); + LogMsalErrorTelemetryToClient(ex, telemetryEventDetails, telemetryClients); throw; } finally @@ -120,12 +121,27 @@ public async Task RunAsync(CancellationToken cancellationT } } - private void LogErrorTelemetryToClient(string errorCode, MsalTelemetryEventDetails telemetryEventDetails, ITelemetryClient[] telemetryClients) + private void LogMsalErrorTelemetryToClient(Exception ex, MsalTelemetryEventDetails telemetryEventDetails, ITelemetryClient[] telemetryClients) { if (telemetryClients.HasEnabledClients(TelemetryConstants.AcquireTokenEventName)) { telemetryEventDetails.SetProperty(TelemetryConstants.Succeeded, false); - telemetryEventDetails.SetProperty(TelemetryConstants.ErrorCode, errorCode); + telemetryEventDetails.SetProperty(TelemetryConstants.ErrorMessage, ex.Message); + + if (ex is MsalClientException clientException) + { + telemetryEventDetails.SetProperty(TelemetryConstants.ErrorCode, clientException.ErrorCode); + return; + } + + if (ex is MsalServiceException serviceException) + { + telemetryEventDetails.SetProperty(TelemetryConstants.ErrorCode, serviceException.ErrorCode); + telemetryEventDetails.SetProperty(TelemetryConstants.StsErrorCode, serviceException.ErrorCodes?.FirstOrDefault()); + return; + } + + telemetryEventDetails.SetProperty(TelemetryConstants.ErrorCode, ex.GetType().ToString()); } } @@ -139,12 +155,66 @@ private void LogSuccessfulTelemetryToClient(AuthenticationResult authenticationR telemetryEventDetails.SetProperty(TelemetryConstants.DurationInCache, authenticationResult.AuthenticationResultMetadata.DurationInCacheInMs); telemetryEventDetails.SetProperty(TelemetryConstants.DurationInHttp, authenticationResult.AuthenticationResultMetadata.DurationInHttpInMs); telemetryEventDetails.SetProperty(TelemetryConstants.Succeeded, true); - telemetryEventDetails.SetProperty(TelemetryConstants.PopToken, authenticationResult.TokenType.Equals(Constants.PoPTokenType)); + telemetryEventDetails.SetProperty(TelemetryConstants.TokenType, (int)AuthenticationRequestParameters.RequestContext.ApiEvent.TokenType); telemetryEventDetails.SetProperty(TelemetryConstants.RemainingLifetime, (authenticationResult.ExpiresOn - DateTime.Now).TotalMilliseconds); telemetryEventDetails.SetProperty(TelemetryConstants.ActivityId, authenticationResult.CorrelationId); + + if (authenticationResult.AuthenticationResultMetadata.RefreshOn.HasValue) + { + telemetryEventDetails.SetProperty(TelemetryConstants.RefreshOn, DateTimeHelpers.DateTimeToUnixTimestampMilliseconds(authenticationResult.AuthenticationResultMetadata.RefreshOn.Value)); + } + telemetryEventDetails.SetProperty(TelemetryConstants.AssertionType, (int)AuthenticationRequestParameters.RequestContext.ApiEvent.AssertionType); + telemetryEventDetails.SetProperty(TelemetryConstants.Endpoint, AuthenticationRequestParameters.Authority.AuthorityInfo.CanonicalAuthority.ToString()); + telemetryEventDetails.SetProperty(TelemetryConstants.CacheLevel, (int)GetCacheLevel(authenticationResult)); + ParseScopesForTelemetry(telemetryEventDetails); + } + } + + private void ParseScopesForTelemetry(MsalTelemetryEventDetails telemetryEventDetails) + { + if (AuthenticationRequestParameters.Scope.Count > 0) + { + string firstScope = AuthenticationRequestParameters.Scope.First(); + + if (Uri.IsWellFormedUriString(firstScope, UriKind.Absolute)) + { + Uri firstScopeAsUri = new Uri(firstScope); + telemetryEventDetails.SetProperty(TelemetryConstants.Resource, $"{firstScopeAsUri.Scheme}://{firstScopeAsUri.Host}"); + + StringBuilder stringBuilder = new StringBuilder(); + + foreach (string scope in AuthenticationRequestParameters.Scope) + { + var splitString = scope.Split(new[] { firstScopeAsUri.Host }, StringSplitOptions.None); + string scopeToAppend = splitString.Count() > 1 ? splitString[1].TrimStart('/') + " " : splitString.FirstOrDefault(); + stringBuilder.Append(scopeToAppend); + } + + telemetryEventDetails.SetProperty(TelemetryConstants.Scopes, stringBuilder.ToString().TrimEnd(' ')); + } + else + { + telemetryEventDetails.SetProperty(TelemetryConstants.Scopes, AuthenticationRequestParameters.Scope.AsSingleString()); + } } } + private CacheLevel GetCacheLevel(AuthenticationResult authenticationResult) + { + if (authenticationResult.AuthenticationResultMetadata.TokenSource == TokenSource.Cache) //Check if token source is cache + { + if (AuthenticationRequestParameters.RequestContext.ApiEvent.CacheLevel > CacheLevel.Unknown) //Check if cache has indicated which level was used + { + return AuthenticationRequestParameters.RequestContext.ApiEvent.CacheLevel; + } + + //If no level was used, set to unknown + return CacheLevel.Unknown; + } + + return CacheLevel.None; + } + private static void LogMetricsFromAuthResult(AuthenticationResult authenticationResult, ILoggerAdapter logger) { if (logger.IsLoggingEnabled(LogLevel.Always)) @@ -193,6 +263,7 @@ private ApiEvent InitializeApiEvent(string accountId) apiEvent.IsLegacyCacheEnabled = AuthenticationRequestParameters.RequestContext.ServiceBundle.Config.LegacyCacheCompatibilityEnabled; apiEvent.CacheInfo = CacheRefreshReason.NotApplicable; apiEvent.TokenType = AuthenticationRequestParameters.AuthenticationScheme.TelemetryTokenType; + apiEvent.AssertionType = GetAssertionType(); // Give derived classes the ability to add or modify fields in the telemetry as needed. EnrichTelemetryApiEvent(apiEvent); @@ -200,6 +271,32 @@ private ApiEvent InitializeApiEvent(string accountId) return apiEvent; } + private AssertionType GetAssertionType() + { + if (ServiceBundle.Config.IsManagedIdentity || + ServiceBundle.Config.AppTokenProvider != null) + { + return AssertionType.ManagedIdentity; + } + + if (ServiceBundle.Config.ClientCredential != null) + { + if (ServiceBundle.Config.ClientCredential.AssertionType == AssertionType.CertificateWithoutSni) + { + if (ServiceBundle.Config.SendX5C) + { + return AssertionType.CertificateWithSni; + } + + return AssertionType.CertificateWithoutSni; + } + + return ServiceBundle.Config.ClientCredential.AssertionType; + } + + return AssertionType.None; + } + protected async Task CacheTokenResponseAndCreateAuthenticationResultAsync(MsalTokenResponse msalTokenResponse) { // developer passed in user object. diff --git a/src/client/Microsoft.Identity.Client/MsalServiceException.cs b/src/client/Microsoft.Identity.Client/MsalServiceException.cs index 9b1f8bbb28..14dba3a357 100644 --- a/src/client/Microsoft.Identity.Client/MsalServiceException.cs +++ b/src/client/Microsoft.Identity.Client/MsalServiceException.cs @@ -221,6 +221,11 @@ public HttpResponseHeaders Headers /// internal string SubError { get; set; } + /// + /// A list of STS-specific error codes that can help in diagnostics. + /// + internal string[] ErrorCodes { get; set; } + /// /// As per discussion with Evo, AAD /// diff --git a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs index 233e649170..63e4671ce0 100644 --- a/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs +++ b/src/client/Microsoft.Identity.Client/MsalServiceExceptionFactory.cs @@ -58,6 +58,7 @@ internal static MsalServiceException FromHttpResponse( ex.Claims = oAuth2Response?.Claims; ex.CorrelationId = oAuth2Response?.CorrelationId; ex.SubError = oAuth2Response?.SubError; + ex.ErrorCodes = oAuth2Response?.ErrorCodes; return ex; } diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/AssertionType.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/AssertionType.cs new file mode 100644 index 0000000000..c6e90fb234 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/AssertionType.cs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +namespace Microsoft.Identity.Client.TelemetryCore +{ + internal enum AssertionType + { + None = 0, + CertificateWithoutSni = 1, + CertificateWithSni = 2, + Secret = 3, + ClientAssertion = 4, + ManagedIdentity = 5 + } +} diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs index 646c4a9bae..50cb36934d 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/Internal/Events/ApiEvent.cs @@ -3,6 +3,7 @@ using System; using Microsoft.Identity.Client.AuthScheme; +using Microsoft.Identity.Client.Cache; using Microsoft.Identity.Client.Region; namespace Microsoft.Identity.Client.TelemetryCore.Internal.Events @@ -127,9 +128,12 @@ public string TokenTypeString get => TokenType.HasValue ? TokenType.Value.ToString("D") : null; } + public AssertionType AssertionType { get; set; } + + public CacheLevel CacheLevel { get; set; } + public static bool IsLongRunningObo(ApiIds apiId) => apiId == ApiIds.InitiateLongRunningObo || apiId == ApiIds.AcquireTokenInLongRunningObo; public static bool IsOnBehalfOfRequest(ApiIds apiId) => apiId == ApiIds.AcquireTokenOnBehalfOf || IsLongRunningObo(apiId); - } } diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryClient/TelemetryData.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryClient/TelemetryData.cs new file mode 100644 index 0000000000..5fd08b1af4 --- /dev/null +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryClient/TelemetryData.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Microsoft.Identity.Client.Cache; +using Microsoft.IdentityModel.Abstractions; + +namespace Microsoft.Identity.Client.TelemetryCore.TelemetryClient +{ + /// + /// Stores details to log to the . + /// + public class TelemetryData + { + /// + /// Type of cache used. This data is captured from MSAL or Microsoft.Identity.Web to log to telemetry. + /// + public CacheLevel CacheLevel { get; set; } = CacheLevel.None; + } +} diff --git a/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs index 94ac646427..db801d17e1 100644 --- a/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs +++ b/src/client/Microsoft.Identity.Client/TelemetryCore/TelemetryConstants.cs @@ -22,17 +22,24 @@ internal static class TelemetryConstants public const string ConfigurationUpdateEventName = "config_update"; public const string MsalVersion = "MsalVersion"; public const string RemainingLifetime = "RemainingLifetime"; - public const string PopToken = "PopToken"; + public const string TokenType = "TokenType"; public const string TokenSource = "TokenSource"; public const string CacheInfoTelemetry = "CacheInfoTelemetry"; public const string ErrorCode = "ErrorCode"; + public const string StsErrorCode = "StsErrorCode"; + public const string ErrorMessage = "ErrorMessage"; public const string Duration = "Duration"; public const string Succeeded = "Succeeded"; public const string DurationInCache = "DurationInCache"; public const string DurationInHttp = "DurationInHttp"; public const string ActivityId = "ActivityId"; public const string Resource = "Resource"; + public const string RefreshOn = "RefreshOn"; + public const string CacheLevel = "CacheLevel"; + public const string AssertionType = "AssertionType"; + public const string Endpoint = "Endpoint"; + public const string Scopes = "Scopes"; -#endregion + #endregion } } diff --git a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs index dc4eea6fbf..ab3a2d8808 100644 --- a/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs +++ b/src/client/Microsoft.Identity.Client/TokenCache.ITokenCacheInternal.cs @@ -18,6 +18,7 @@ using Microsoft.Identity.Client.Internal.Requests; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Client.TelemetryCore.Internal.Events; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; using Microsoft.Identity.Client.Utils; namespace Microsoft.Identity.Client @@ -266,8 +267,8 @@ async Task> IToke requestParams.RequestContext.ApiEvent.DurationInCacheInMs += sw.ElapsedMilliseconds; DumpCacheToLogs(requestParams); - } + #pragma warning disable CS0618 // Type or member is obsolete HasStateChanged = false; #pragma warning restore CS0618 // Type or member is obsolete @@ -1216,7 +1217,7 @@ async Task ITokenCacheInternal.StopLongRunningOboProcessAsync(string longR requestScopes: requestParameters.Scope, requestTenantId: requestParameters.AuthorityManager.OriginalAuthority.TenantId, identityLogger: requestParameters.RequestContext.Logger.IdentityLogger, - piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); + piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); await tokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); } @@ -1301,7 +1302,7 @@ async Task ITokenCacheInternal.RemoveAccountAsync(IAccount account, Authenticati requestScopes: requestParameters.Scope, requestTenantId: requestParameters.AuthorityManager.OriginalAuthority.TenantId, identityLogger: requestParameters.RequestContext.Logger.IdentityLogger, - piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); + piiLoggingEnabled: requestParameters.RequestContext.Logger.PiiLoggingEnabled); await tokenCacheInternal.OnAfterAccessAsync(args).ConfigureAwait(false); } diff --git a/src/client/Microsoft.Identity.Client/TokenCacheNotificationArgs.cs b/src/client/Microsoft.Identity.Client/TokenCacheNotificationArgs.cs index f8db180c66..10b7e3de45 100644 --- a/src/client/Microsoft.Identity.Client/TokenCacheNotificationArgs.cs +++ b/src/client/Microsoft.Identity.Client/TokenCacheNotificationArgs.cs @@ -5,6 +5,7 @@ using System.Collections; using System.Collections.Generic; using System.Threading; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; using Microsoft.IdentityModel.Abstractions; namespace Microsoft.Identity.Client @@ -127,8 +128,8 @@ public TokenCacheNotificationArgs( // only use this constructor in product co IEnumerable requestScopes, string requestTenantId, IIdentityLogger identityLogger, - bool piiLoggingEnabled) - + bool piiLoggingEnabled, + TelemetryData telemetryData = null) { TokenCache = tokenCache; ClientId = clientId; @@ -144,6 +145,7 @@ public TokenCacheNotificationArgs( // only use this constructor in product co SuggestedCacheExpiry = suggestedCacheExpiry; IdentityLogger = identityLogger; PiiLoggingEnabled = piiLoggingEnabled; + TelemetryData = telemetryData?? new TelemetryData(); } /// @@ -248,5 +250,10 @@ public TokenCacheNotificationArgs( // only use this constructor in product co /// Boolean used to determine if Personally Identifiable Information (PII) logging is enabled. /// public bool PiiLoggingEnabled { get; } + + /// + /// Cache Details contains the details of L1/ L2 cache for telemetry logging. + /// + public TelemetryData TelemetryData { get; } } } diff --git a/tests/Microsoft.Identity.Test.Common/Core/Helpers/RSACertificatePopCryptoProvider.cs b/tests/Microsoft.Identity.Test.Common/Core/Helpers/RSACertificatePopCryptoProvider.cs new file mode 100644 index 0000000000..4a3654b2c2 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Common/Core/Helpers/RSACertificatePopCryptoProvider.cs @@ -0,0 +1,51 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Security.Cryptography.X509Certificates; +using System.Security.Cryptography; +using System.Text; +using Microsoft.Identity.Client.AuthScheme.PoP; +using Microsoft.Identity.Client.Utils; + +namespace Microsoft.Identity.Test.Common.Core.Helpers +{ + public class RSACertificatePopCryptoProvider : IPoPCryptoProvider + { + private readonly X509Certificate2 _cert; + + public RSACertificatePopCryptoProvider(X509Certificate2 cert) + { + _cert = cert ?? throw new ArgumentNullException(nameof(cert)); + + RSA provider = _cert.GetRSAPublicKey(); + RSAParameters publicKeyParams = provider.ExportParameters(false); + CannonicalPublicKeyJwk = ComputeCannonicalJwk(publicKeyParams); + } + + public byte[] Sign(byte[] payload) + { + using (RSA key = _cert.GetRSAPrivateKey()) + { + return key.SignData( + payload, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + } + } + + public string CannonicalPublicKeyJwk { get; } + + public string CryptographicAlgorithm { get => "RS256"; } + + /// + /// Creates the canonical representation of the JWK. See https://tools.ietf.org/html/rfc7638#section-3 + /// The number of parameters as well as the lexicographic order is important, as this string will be hashed to get a thumbprint + /// + private static string ComputeCannonicalJwk(RSAParameters rsaPublicKey) + { + return $@"{{""e"":""{Base64UrlHelpers.Encode(rsaPublicKey.Exponent)}"",""kty"":""RSA"",""n"":""{Base64UrlHelpers.Encode(rsaPublicKey.Modulus)}""}}"; + } + } +} diff --git a/tests/Microsoft.Identity.Test.Common/Core/Helpers/TestTelemetryClient.cs b/tests/Microsoft.Identity.Test.Common/Core/Helpers/TestTelemetryClient.cs new file mode 100644 index 0000000000..50a2f25f29 --- /dev/null +++ b/tests/Microsoft.Identity.Test.Common/Core/Helpers/TestTelemetryClient.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Identity.Client.TelemetryCore; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; +using Microsoft.IdentityModel.Abstractions; + +namespace Microsoft.Identity.Test.Common.Core.Helpers +{ + internal class TestTelemetryClient : ITelemetryClient + { + public MsalTelemetryEventDetails TestTelemetryEventDetails { get; set; } + + public TestTelemetryClient(string clientId) + { + ClientId = clientId; + } + + public string ClientId { get; set; } + + public void Initialize() + { + + } + + public bool IsEnabled() + { + return true; + } + + public bool IsEnabled(string eventName) + { + return TelemetryConstants.AcquireTokenEventName.Equals(eventName); + } + + public void TrackEvent(TelemetryEventDetails eventDetails) + { + TestTelemetryEventDetails = (MsalTelemetryEventDetails)eventDetails; + } + + public void TrackEvent(string eventName, IDictionary stringProperties = null, IDictionary longProperties = null, IDictionary boolProperties = null, IDictionary dateTimeProperties = null, IDictionary doubleProperties = null, IDictionary guidProperties = null) + { + throw new NotImplementedException(); + } + } +} diff --git a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/PoPTests.cs b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/PoPTests.cs index fc7a2f9a4e..fa74861907 100644 --- a/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/PoPTests.cs +++ b/tests/Microsoft.Identity.Test.Integration.netfx/HeadlessTests/PoPTests.cs @@ -29,6 +29,10 @@ using Microsoft.IdentityModel.Tokens; using Microsoft.VisualStudio.TestTools.UnitTesting; using Microsoft.Identity.Test.Common.Core.Helpers; +using Microsoft.IdentityModel.Abstractions; +using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; +using Microsoft.Identity.Client.AuthScheme; +using Microsoft.Identity.Client.TelemetryCore; namespace Microsoft.Identity.Test.Integration.HeadlessTests { @@ -258,11 +262,13 @@ await VerifyPoPTokenAsync( [TestMethod] public async Task PopTestWithRSAAsync() { + var telemetryClient = new TestTelemetryClient(TestConstants.ClientId); var confidentialApp = ConfidentialClientApplicationBuilder .Create(PublicCloudConfidentialClientID) .WithExperimentalFeatures() .WithAuthority(PublicCloudTestAuthority) .WithClientSecret(s_publicCloudCcaSecret) + .WithTelemetryClient(telemetryClient) .Build(); //RSA provider @@ -281,6 +287,10 @@ await VerifyPoPTokenAsync( ProtectedUrl, HttpMethod.Get, result).ConfigureAwait(false); + + MsalTelemetryEventDetails eventDetails = telemetryClient.TestTelemetryEventDetails; + Assert.IsNotNull(eventDetails); + Assert.AreEqual(Convert.ToInt64(TokenType.Pop), eventDetails.Properties[TelemetryConstants.TokenType]); } [TestMethod] @@ -590,42 +600,4 @@ private static string CreateRsaKeyId(RSAParameters rsaParameters) return Base64UrlEncoder.Encode(sha2.ComputeHash(kidBytes)); } } - - public class RSACertificatePopCryptoProvider : IPoPCryptoProvider - { - private readonly X509Certificate2 _cert; - - public RSACertificatePopCryptoProvider(X509Certificate2 cert) - { - _cert = cert ?? throw new ArgumentNullException(nameof(cert)); - - RSA provider = _cert.GetRSAPublicKey(); - RSAParameters publicKeyParams = provider.ExportParameters(false); - CannonicalPublicKeyJwk = ComputeCannonicalJwk(publicKeyParams); - } - - public byte[] Sign(byte[] payload) - { - using (RSA key = _cert.GetRSAPrivateKey()) - { - return key.SignData( - payload, - HashAlgorithmName.SHA256, - RSASignaturePadding.Pkcs1); - } - } - - public string CannonicalPublicKeyJwk { get; } - - public string CryptographicAlgorithm { get => "RS256"; } - - /// - /// Creates the canonical representation of the JWK. See https://tools.ietf.org/html/rfc7638#section-3 - /// The number of parameters as well as the lexicographic order is important, as this string will be hashed to get a thumbprint - /// - private static string ComputeCannonicalJwk(RSAParameters rsaPublicKey) - { - return $@"{{""e"":""{Base64UrlHelpers.Encode(rsaPublicKey.Exponent)}"",""kty"":""RSA"",""n"":""{Base64UrlHelpers.Encode(rsaPublicKey.Modulus)}""}}"; - } - } } diff --git a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs index 052e5e7113..fb2b3e25ad 100644 --- a/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/ManagedIdentityTests/AppServiceTests.cs @@ -16,7 +16,6 @@ namespace Microsoft.Identity.Test.Unit.ManagedIdentityTests { - [TestClass] public class AppServiceTests : TestBase { diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs index 3d5dde89f4..0131a1e40f 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/ConfidentialClientApplicationTests.cs @@ -29,7 +29,7 @@ namespace Microsoft.Identity.Test.Unit.PublicApiTests [TestClass] [DeploymentItem(@"Resources\valid.crtfile")] [DeploymentItem("Resources\\OpenidConfiguration-QueryParams-B2C.json")] - public class ConfidentialClientApplicationTests + public class ConfidentialClientApplicationTests : TestBase { private byte[] _serializedCache; @@ -1913,8 +1913,6 @@ public async Task ValidateAppTokenProviderAsync() Assert.AreEqual(4, callbackInvoked); } - - private AppTokenProviderResult GetAppTokenProviderResult(string differentScopesForAt = "", long? refreshIn = 1000) { var token = new AppTokenProviderResult(); diff --git a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/TelemetryClientTests.cs b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/TelemetryClientTests.cs index a47a46e686..6cf9003070 100644 --- a/tests/Microsoft.Identity.Test.Unit/PublicApiTests/TelemetryClientTests.cs +++ b/tests/Microsoft.Identity.Test.Unit/PublicApiTests/TelemetryClientTests.cs @@ -4,16 +4,28 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Net; +using System.Net.Sockets; +using System.Security.Cryptography.X509Certificates; using System.Threading; using System.Threading.Tasks; using Microsoft.Identity.Client; +using Microsoft.Identity.Client.AppConfig; +using Microsoft.Identity.Client.AuthScheme; +using Microsoft.Identity.Client.Cache; +using Microsoft.Identity.Client.Extensibility; +using Microsoft.Identity.Client.ManagedIdentity; using Microsoft.Identity.Client.TelemetryCore; using Microsoft.Identity.Client.TelemetryCore.TelemetryClient; +using Microsoft.Identity.Client.Utils; using Microsoft.Identity.Test.Common.Core.Helpers; using Microsoft.Identity.Test.Common.Core.Mocks; using Microsoft.Identity.Test.Common.Mocks; +using Microsoft.Identity.Test.Unit.TelemetryTests; using Microsoft.IdentityModel.Abstractions; using Microsoft.VisualStudio.TestTools.UnitTesting; +using NSubstitute; +using static Microsoft.Identity.Test.Common.Core.Helpers.ManagedIdentityTestUtil; namespace Microsoft.Identity.Test.Unit.PublicApiTests { @@ -22,12 +34,12 @@ public class TelemetryClientTests : TestBase { private MockHttpAndServiceBundle _harness; private ConfidentialClientApplication _cca; - private TelemetryClient _telemetryClient; + private TestTelemetryClient _telemetryClient; [TestInitialize] public override void TestInitialize() { - _telemetryClient = new TelemetryClient(TestConstants.ClientId); + _telemetryClient = new TestTelemetryClient(TestConstants.ClientId); base.TestInitialize(); } @@ -37,18 +49,6 @@ public override void TestCleanup() base.TestCleanup(); } - [TestMethod] - public void TelemetryClientExperimental() - { - var e = AssertException.Throws(() => ConfidentialClientApplicationBuilder - .Create(TestConstants.ClientId) - .WithClientSecret("secret") - .WithTelemetryClient(_telemetryClient) - .Build()); - - Assert.AreEqual(MsalError.ExperimentalFeature, e.ErrorCode); - } - [TestMethod] public void TelemetryClientListNull() { @@ -88,17 +88,17 @@ public void TelemetryClientNoArg() Assert.IsNotNull(cca); } - [TestMethod] + [TestMethod] public async Task AcquireTokenSuccessfulTelemetryTestAsync() { using (_harness = CreateTestHarness()) { _harness.HttpManager.AddInstanceDiscoveryMockHandler(); - + CreateApplication(); _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); - // Acquire token interactively + // Acquire token for client with scope var result = await _cca.AcquireTokenForClient(TestConstants.s_scope) .WithAuthority(TestConstants.AuthorityUtidTenant) .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); @@ -106,7 +106,17 @@ public async Task AcquireTokenSuccessfulTelemetryTestAsync() Assert.IsNotNull(result); MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; - AssertLoggedTelemetry(result, eventDetails, TokenSource.IdentityProvider, CacheRefreshReason.NoCachedAccessToken); + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.None, + TestConstants.s_scope.AsSingleString(), + null); // Acquire token silently var account = (await _cca.GetAccountsAsync().ConfigureAwait(false)).Single(); @@ -116,7 +126,283 @@ public async Task AcquireTokenSuccessfulTelemetryTestAsync() Assert.IsNotNull(result); eventDetails = _telemetryClient.TestTelemetryEventDetails; - AssertLoggedTelemetry(result, eventDetails, TokenSource.Cache, CacheRefreshReason.NotApplicable); + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.Cache, + CacheRefreshReason.NotApplicable, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.Unknown, + TestConstants.s_scope.AsSingleString(), + null); + + _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + + // Acquire token forclient with resource + result = await _cca.AcquireTokenForClient(new[] { TestConstants.DefaultGraphScope }) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + eventDetails = _telemetryClient.TestTelemetryEventDetails; + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.None, + null, + "https://graph.microsoft.com"); + } + } + + [TestMethod] + [DataRow(new[] { "https://graph.microsoft.com/.default" }, "https://graph.microsoft.com", ".default")] + [DataRow(new[] { "https://graph.microsoft.com/User.Read", "https://graph.microsoft.com/Mail.Read" }, "https://graph.microsoft.com", "User.Read Mail.Read")] + [DataRow(new[] { "api://23c64cd8-21e4-41dd-9756-ab9e2c23f58c/access_as_user" }, "api://23c64cd8-21e4-41dd-9756-ab9e2c23f58c", "access_as_user")] + [DataRow(new[] { "User.Read", "Mail.Read" }, null, "User.Read Mail.Read")] + [DataRow(new[] { "https://sharepoint.com/scope" }, "https://sharepoint.com", "scope")] + [DataRow(new[] { "offline_access", "openid", "profile" }, null, "offline_access openid profile")] + [DataRow(new[] { "https://graph.microsoft.com/.default", "User.Read" }, "https://graph.microsoft.com", ".default User.Read")] + public async Task AcquireTokenSuccessfulTelemetryTestForScopesAsync(IEnumerable input, string expectedResource, string expectedScope) + { + using (_harness = CreateTestHarness()) + { + _harness.HttpManager.AddInstanceDiscoveryMockHandler(); + + CreateApplication(); + _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + + // Acquire token for client with scope + var result = await _cca.AcquireTokenForClient(input) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.None, + expectedScope, + expectedResource); + } + } + + [TestMethod] + [DataRow(1)] + [DataRow(2)] + [DataRow(3)] + [DataRow(4)] + [DataRow(5)] + public async Task AcquireTokenAssertionTypeTelemetryTestAsync(int assertionType) + { + using (_harness = CreateTestHarness()) + { + _harness.HttpManager.AddInstanceDiscoveryMockHandler(); + + CreateApplication((AssertionType)assertionType); + if (assertionType != 5) + { + _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + } + + var result = await _cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + (AssertionType)assertionType, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.None, + TestConstants.s_scope.AsSingleString(), + null); + } + } + + [TestMethod] + public async Task AcquireTokenCacheTelemetryTestAsync() + { + using (_harness = CreateTestHarness()) + { + //Create app + CacheLevel cacheLevel = CacheLevel.L1Cache; + _harness.HttpManager.AddInstanceDiscoveryMockHandler(); + CreateApplication(); + + _harness.HttpManager.AddMockHandlerSuccessfulClientCredentialTokenResponseMessage(); + + //Configure cache + _cca.AppTokenCache.SetBeforeAccess((args) => + { + args.TelemetryData.CacheLevel = cacheLevel; + }); + + _cca.AppTokenCache.SetAfterAccess((args) => + { + args.TelemetryData.CacheLevel = cacheLevel; + }); + + //Acquire Token + var result = await _cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; + + //Validate telemetry + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.None, + TestConstants.s_scope.AsSingleString(), + null); + + //Update cache type + cacheLevel = CacheLevel.L1Cache; + + //Acquire Token + result = await _cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + eventDetails = _telemetryClient.TestTelemetryEventDetails; + + //Validate telemetry + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.Cache, + CacheRefreshReason.NotApplicable, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.L1Cache, + TestConstants.s_scope.AsSingleString(), + null); + + //Update cache type again + cacheLevel = CacheLevel.L2Cache; + + //Acquire Token + result = await _cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + eventDetails = _telemetryClient.TestTelemetryEventDetails; + + //Validate telemetry + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.Cache, + CacheRefreshReason.NotApplicable, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.L2Cache, + TestConstants.s_scope.AsSingleString(), + null); + + //Simulate the cache not providing a value + cacheLevel = CacheLevel.None; + + //Acquire Token + result = await _cca.AcquireTokenForClient(TestConstants.s_scope) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None).ConfigureAwait(false); + + Assert.IsNotNull(result); + + eventDetails = _telemetryClient.TestTelemetryEventDetails; + + //Validate telemetry + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.Cache, + CacheRefreshReason.NotApplicable, + AssertionType.Secret, + TestConstants.AuthorityUtidTenant, + TokenType.Bearer, + CacheLevel.Unknown, + TestConstants.s_scope.AsSingleString(), + null); + } + } + + [TestMethod] + public async Task AcquireTokenWithMSITelemetryTestAsync() + { + using (new EnvVariableContext()) + using (_harness = CreateTestHarness()) + { + string endpoint = "http://localhost:40342/metadata/identity/oauth2/token"; + string resource = "https://management.azure.com"; + + Environment.SetEnvironmentVariable("MSI_ENDPOINT", endpoint); + + var mia = ManagedIdentityApplicationBuilder + .Create(ManagedIdentityId.SystemAssigned) + .WithExperimentalFeatures() + .WithHttpManager(_harness.HttpManager) + .WithTelemetryClient(_telemetryClient) + .Build(); + + _harness.HttpManager.AddManagedIdentityMockHandler( + endpoint, + resource, + MockHelpers.GetMsiSuccessfulResponse(), + ManagedIdentitySource.CloudShell); + + var result = await mia.AcquireTokenForManagedIdentity(resource) + .ExecuteAsync().ConfigureAwait(false); + + Assert.IsNotNull(result); + + MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; + AssertLoggedTelemetry( + result, + eventDetails, + TokenSource.IdentityProvider, + CacheRefreshReason.NoCachedAccessToken, + AssertionType.ManagedIdentity, + "https://login.microsoftonline.com/managed_identity/", + TokenType.Bearer, + CacheLevel.None, + null, + resource); } } @@ -130,6 +416,7 @@ public async Task AcquireTokenUnSuccessfulTelemetryTestAsync() CreateApplication(); _harness.HttpManager.AddTokenResponse(TokenResponseType.InvalidClient); + //Test for MsalServiceException MsalServiceException ex = await AssertException.TaskThrowsAsync( () => _cca.AcquireTokenForClient(TestConstants.s_scope) .WithAuthority(TestConstants.AuthorityUtidTenant) @@ -140,69 +427,149 @@ public async Task AcquireTokenUnSuccessfulTelemetryTestAsync() MsalTelemetryEventDetails eventDetails = _telemetryClient.TestTelemetryEventDetails; Assert.AreEqual(ex.ErrorCode, eventDetails.Properties[TelemetryConstants.ErrorCode]); + Assert.AreEqual(ex.Message, eventDetails.Properties[TelemetryConstants.ErrorMessage]); + Assert.AreEqual(ex.ErrorCodes.AsSingleString(), eventDetails.Properties[TelemetryConstants.StsErrorCode]); + Assert.IsFalse((bool?)eventDetails.Properties[TelemetryConstants.Succeeded]); + + //Test for MsalClientException + _harness.HttpManager.AddTokenResponse(TokenResponseType.InvalidClient); + + MsalClientException exClient = await AssertException.TaskThrowsAsync( + () => _cca.AcquireTokenForClient(null) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None)).ConfigureAwait(false); + + Assert.IsNotNull(exClient); + Assert.IsNotNull(exClient.ErrorCode); + + eventDetails = _telemetryClient.TestTelemetryEventDetails; + Assert.AreEqual(exClient.ErrorCode, eventDetails.Properties[TelemetryConstants.ErrorCode]); + Assert.AreEqual(exClient.Message, eventDetails.Properties[TelemetryConstants.ErrorMessage]); Assert.IsFalse((bool?)eventDetails.Properties[TelemetryConstants.Succeeded]); } } - private void AssertLoggedTelemetry(AuthenticationResult authenticationResult, MsalTelemetryEventDetails eventDetails, TokenSource tokenSource, CacheRefreshReason cacheRefreshReason) + [TestMethod] + public async Task AcquireTokenGenericErrorTelemetryTestAsync() { - Assert.IsNotNull(eventDetails); - Assert.AreEqual(Convert.ToInt64(cacheRefreshReason), eventDetails.Properties[TelemetryConstants.CacheInfoTelemetry]); - Assert.AreEqual(Convert.ToInt64(tokenSource), eventDetails.Properties[TelemetryConstants.TokenSource]); - Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationTotalInMs, eventDetails.Properties[TelemetryConstants.Duration]); - Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationInHttpInMs, eventDetails.Properties[TelemetryConstants.DurationInHttp]); - Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationInCacheInMs, eventDetails.Properties[TelemetryConstants.DurationInCache]); - Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationTotalInMs, eventDetails.Properties[TelemetryConstants.Duration]); - } + IMsalHttpClientFactory factoryThatThrows = Substitute.For(); + factoryThatThrows.When(x => x.GetHttpClient()).Do(x => { throw new SocketException(0); }); - private void CreateApplication() - { - _cca = ConfidentialClientApplicationBuilder + var cca = ConfidentialClientApplicationBuilder .Create(TestConstants.ClientId) .WithClientSecret(TestConstants.ClientSecret) - .WithHttpManager(_harness.HttpManager) + .WithHttpClientFactory(factoryThatThrows) .WithExperimentalFeatures() .WithTelemetryClient(_telemetryClient) .BuildConcrete(); - TokenCacheHelper.PopulateCache(_cca.UserTokenCacheInternal.Accessor); + MsalClientException exClient = await AssertException.TaskThrowsAsync( + () => cca.AcquireTokenForClient(null) + .WithAuthority(TestConstants.AuthorityUtidTenant) + .ExecuteAsync(CancellationToken.None)).ConfigureAwait(false); + + Assert.IsNotNull(exClient); + Assert.IsNotNull(exClient.ErrorCode); + + var eventDetails = _telemetryClient.TestTelemetryEventDetails; + Assert.AreEqual(exClient.ErrorCode, eventDetails.Properties[TelemetryConstants.ErrorCode]); + Assert.AreEqual(exClient.Message, eventDetails.Properties[TelemetryConstants.ErrorMessage]); + Assert.IsFalse((bool?)eventDetails.Properties[TelemetryConstants.Succeeded]); } - } - internal class TelemetryClient : ITelemetryClient - { - public MsalTelemetryEventDetails TestTelemetryEventDetails { get; set; } - - public TelemetryClient(string clientId) + private void AssertLoggedTelemetry( + AuthenticationResult authenticationResult, + MsalTelemetryEventDetails eventDetails, + TokenSource tokenSource, + CacheRefreshReason cacheRefreshReason, + AssertionType assertionType, + string endpoint, + TokenType? tokenType, + CacheLevel cacheLevel, + string scopes, + string resource) { - ClientId = clientId; - } + Assert.IsNotNull(eventDetails); + Assert.AreEqual(Convert.ToInt64(cacheRefreshReason), eventDetails.Properties[TelemetryConstants.CacheInfoTelemetry]); + Assert.AreEqual(Convert.ToInt64(tokenSource), eventDetails.Properties[TelemetryConstants.TokenSource]); + Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationTotalInMs, eventDetails.Properties[TelemetryConstants.Duration]); + Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationInHttpInMs, eventDetails.Properties[TelemetryConstants.DurationInHttp]); + Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationInCacheInMs, eventDetails.Properties[TelemetryConstants.DurationInCache]); + Assert.AreEqual(authenticationResult.AuthenticationResultMetadata.DurationTotalInMs, eventDetails.Properties[TelemetryConstants.Duration]); + Assert.AreEqual(Convert.ToInt64(assertionType), eventDetails.Properties[TelemetryConstants.AssertionType]); + Assert.AreEqual(Convert.ToInt64(tokenType), eventDetails.Properties[TelemetryConstants.TokenType]); + Assert.AreEqual(endpoint, eventDetails.Properties[TelemetryConstants.Endpoint]); + Assert.AreEqual(Convert.ToInt64(cacheLevel), eventDetails.Properties[TelemetryConstants.CacheLevel]); - public string ClientId { get; set; } + if (!string.IsNullOrWhiteSpace(scopes)) + { + Assert.AreEqual(scopes, eventDetails.Properties[TelemetryConstants.Scopes]); + } - public void Initialize() - { + if (!string.IsNullOrWhiteSpace(resource)) + { + Assert.AreEqual(resource, eventDetails.Properties[TelemetryConstants.Resource]); + } } - public bool IsEnabled() + private void CreateApplication(AssertionType assertionType = AssertionType.Secret) { - return true; - } + var certificate = new X509Certificate2( + ResourceHelper.GetTestResourceRelativePath("valid_cert.pfx"), + TestConstants.DefaultPassword); - public bool IsEnabled(string eventName) - { - return TelemetryConstants.AcquireTokenEventName.Equals(eventName); - } + switch (assertionType) + { + case AssertionType.Secret: + _cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientSecret(TestConstants.ClientSecret) + .WithHttpManager(_harness.HttpManager) + .WithExperimentalFeatures() + .WithTelemetryClient(_telemetryClient) + .BuildConcrete(); + break; + case AssertionType.CertificateWithoutSni: + _cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(certificate) + .WithHttpManager(_harness.HttpManager) + .WithExperimentalFeatures() + .WithTelemetryClient(_telemetryClient) + .BuildConcrete(); + break; + case AssertionType.CertificateWithSni: + _cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithCertificate(certificate, true) + .WithHttpManager(_harness.HttpManager) + .WithExperimentalFeatures() + .WithTelemetryClient(_telemetryClient) + .BuildConcrete(); + break; + case AssertionType.ClientAssertion: + _cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithClientAssertion(TestConstants.DefaultClientAssertion) + .WithHttpManager(_harness.HttpManager) + .WithExperimentalFeatures() + .WithTelemetryClient(_telemetryClient) + .BuildConcrete(); + break; + case AssertionType.ManagedIdentity: + _cca = ConfidentialClientApplicationBuilder + .Create(TestConstants.ClientId) + .WithAppTokenProvider((AppTokenProviderParameters parameters) => { return Task.FromResult(GetAppTokenProviderResult()); }) + .WithHttpManager(_harness.HttpManager) + .WithExperimentalFeatures() + .WithTelemetryClient(_telemetryClient) + .BuildConcrete(); + break; + } - public void TrackEvent(TelemetryEventDetails eventDetails) - { - TestTelemetryEventDetails = (MsalTelemetryEventDetails) eventDetails; - } - public void TrackEvent(string eventName, IDictionary stringProperties = null, IDictionary longProperties = null, IDictionary boolProperties = null, IDictionary dateTimeProperties = null, IDictionary doubleProperties = null, IDictionary guidProperties = null) - { - throw new NotImplementedException(); + TokenCacheHelper.PopulateCache(_cca.UserTokenCacheInternal.Accessor); } } } diff --git a/tests/Microsoft.Identity.Test.Unit/TestBase.cs b/tests/Microsoft.Identity.Test.Unit/TestBase.cs index ec1bb6a46b..ee45bd6fb9 100644 --- a/tests/Microsoft.Identity.Test.Unit/TestBase.cs +++ b/tests/Microsoft.Identity.Test.Unit/TestBase.cs @@ -5,6 +5,7 @@ using System.Diagnostics; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Core; +using Microsoft.Identity.Client.Extensibility; using Microsoft.Identity.Client.Internal.Broker; using Microsoft.Identity.Client.OAuth2; using Microsoft.Identity.Test.Common; @@ -126,5 +127,15 @@ internal static IBroker CreateBroker(Type brokerType) throw new NotImplementedException(); } + + internal AppTokenProviderResult GetAppTokenProviderResult(string differentScopesForAt = "", long? refreshIn = 1000) + { + var token = new AppTokenProviderResult(); + token.AccessToken = TestConstants.DefaultAccessToken + differentScopesForAt; //Used to indicate that there is a new access token for a different set of scopes + token.ExpiresInSeconds = 3600; + token.RefreshInSeconds = refreshIn; + + return token; + } } }