• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

microsoft / botbuilder-dotnet / 375151

12 Oct 2023 02:04PM UTC coverage: 73.509% (+0.06%) from 73.448%
375151

push

CI-PR build

web-flow
Add sendx5c parameter to Certificate factory class (#6699)

* Add sendX5c parameter in certificate auth factory

* Add unit tests

* Remove commented property.

* Revert changes in CertificateServiceClientCredentialsFactory

24175 of 32887 relevant lines covered (73.51%)

0.74 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

12.22
/libraries/Microsoft.Bot.Connector/Authentication/MsalAppCredentials.cs
1
// Copyright (c) Microsoft Corporation. All rights reserved.
2
// Licensed under the MIT License.
3

4
using System;
5
using System.Diagnostics;
6
using System.Security.Cryptography.X509Certificates;
7
using System.Threading;
8
using System.Threading.Tasks;
9
using Microsoft.Extensions.Logging;
10
using Microsoft.Identity.Client;
11

12
namespace Microsoft.Bot.Connector.Authentication
13
{
14
    /// <summary>
15
    /// An authentication class that implements <see cref="IAuthenticator"/>, used to acquire tokens for outgoing messages to the channels.
16
    /// </summary>
17
    public class MsalAppCredentials : AppCredentials, IAuthenticator
18
    {
19
        /// <summary>
20
        /// An empty set of credentials.
21
        /// </summary>
22
        public static readonly MsalAppCredentials Empty = new MsalAppCredentials(clientApplication: null, appId: null);
1✔
23

24
        // Semaphore to control concurrency while refreshing tokens from MSAL.
25
        // Whenever a token expires, we want only one request to retrieve a token.
26
        // Cached requests take less than 0.1 millisecond to resolve, so the semaphore doesn't hurt performance under load tests
27
        // unless we have more than 10,000 requests per second, but in that case other things would break first.
28
        private static SemaphoreSlim tokenRefreshSemaphore = new SemaphoreSlim(1, 1);
1✔
29
        private static readonly TimeSpan SemaphoreTimeout = TimeSpan.FromSeconds(10);
1✔
30

31
        // Our MSAL application. Acquires tokens and manages token caching for us.
32
        private readonly IConfidentialClientApplication _clientApplication;
33

34
        private readonly ILogger _logger;
35
        private readonly string _scope;
36
        private readonly string _authority;
37
        private readonly bool _validateAuthority;
38

39
        /// <summary>
40
        /// Initializes a new instance of the <see cref="MsalAppCredentials"/> class.
41
        /// </summary>
42
        /// <param name="clientApplication">The client application to use to acquire tokens.</param>
43
        /// <param name="appId">The Microsoft application Id.</param>
44
        /// <param name="logger">Optional <see cref="ILogger"/>.</param>
45
        /// <param name="authority">Optional authority.</param>
46
        /// <param name="validateAuthority">Whether to validate the authority.</param>
47
        /// <param name="scope">Optional custom scope.</param>
48
        public MsalAppCredentials(IConfidentialClientApplication clientApplication, string appId, string authority = null, string scope = null, bool validateAuthority = true, ILogger logger = null)
49
            : base(null, null, logger, scope)
1✔
50
        {
51
            MicrosoftAppId = appId;
1✔
52
            _clientApplication = clientApplication;
1✔
53
            _logger = logger;
1✔
54
            _scope = scope;
1✔
55
            _authority = authority;
1✔
56
            _validateAuthority = validateAuthority;
1✔
57
        }
1✔
58

59
        /// <summary>
60
        /// Initializes a new instance of the <see cref="MsalAppCredentials"/> class.
61
        /// </summary>
62
        /// <param name="appId">The Microsoft application id.</param>
63
        /// <param name="appPassword">The Microsoft application password.</param>
64
        /// <param name="authority">Optional authority.</param>
65
        /// <param name="validateAuthority">Whether to validate the authority.</param>
66
        /// <param name="scope">Optional custom scope.</param>
67
        /// <param name="logger">Optional <see cref="ILogger"/>.</param>
68
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "Using string overload for legacy compatibility.")]
69
        public MsalAppCredentials(string appId, string appPassword, string authority = null, string scope = null, bool validateAuthority = true, ILogger logger = null)
70
            : this(
×
71
                  clientApplication: null,
×
72
                  appId: appId,
×
73
                  authority: authority,
×
74
                  scope: scope,
×
75
                  validateAuthority: validateAuthority,
×
76
                  logger: logger)
×
77
        {
78
            _clientApplication = ConfidentialClientApplicationBuilder.Create(appId)
×
79
                .WithAuthority(authority ?? OAuthEndpoint, validateAuthority)
×
80
                .WithClientSecret(appPassword)
×
81
                .Build();
×
82
        }
×
83

84
        /// <summary>
85
        /// Initializes a new instance of the <see cref="MsalAppCredentials"/> class.
86
        /// </summary>
87
        /// <param name="appId">The Microsoft application id.</param>
88
        /// <param name="certificate">The certificate to use for authentication.</param>
89
        /// <param name="validateAuthority">Optional switch for whether to validate the authority.</param>
90
        /// <param name="authority">Optional authority.</param>
91
        /// <param name="scope">Optional custom scope.</param>
92
        /// <param name="logger">Optional <see cref="ILogger"/>.</param>
93
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "Using string overload for legacy compatibility.")]
94
        public MsalAppCredentials(string appId, X509Certificate2 certificate, string authority = null, string scope = null, bool validateAuthority = true, ILogger logger = null)
95
            : this(
×
96
                  clientApplication: null,
×
97
                  appId: appId,
×
98
                  authority: authority,
×
99
                  scope: scope,
×
100
                  validateAuthority: validateAuthority,
×
101
                  logger: logger)
×
102
        {
103
            _clientApplication = ConfidentialClientApplicationBuilder.Create(appId)
×
104
                .WithAuthority(authority ?? OAuthEndpoint, validateAuthority)
×
105
                .WithCertificate(certificate)
×
106
                .Build();
×
107
        }
×
108

109
        /// <summary>
110
        /// Initializes a new instance of the <see cref="MsalAppCredentials"/> class.
111
        /// </summary>
112
        /// <param name="appId">The Microsoft application id.</param>
113
        /// <param name="certificate">The certificate to use for authentication.</param>
114
        /// <param name="sendX5c">If true will send the public certificate to Azure AD along with the token request, so that
115
        /// Azure AD can use it to validate the subject name based on a trusted issuer policy.</param>
116
        /// <param name="validateAuthority">Optional switch for whether to validate the authority.</param>
117
        /// <param name="authority">Optional authority.</param>
118
        /// <param name="scope">Optional custom scope.</param>
119
        /// <param name="logger">Optional <see cref="ILogger"/>.</param>
120
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "CA2234:Pass system uri objects instead of strings", Justification = "Using string overload for legacy compatibility.")]
121
        public MsalAppCredentials(string appId, X509Certificate2 certificate, bool sendX5c, string authority = null, string scope = null, bool validateAuthority = true, ILogger logger = null)
122
            : this(
×
123
                  clientApplication: null,
×
124
                  appId: appId,
×
125
                  authority: authority,
×
126
                  scope: scope,
×
127
                  validateAuthority: validateAuthority,
×
128
                  logger: logger)
×
129
        {
130
            _clientApplication = ConfidentialClientApplicationBuilder.Create(appId)
×
131
                .WithAuthority(authority ?? OAuthEndpoint, validateAuthority)
×
132
                .WithCertificate(certificate, sendX5c)
×
133
                .Build();
×
134
        }
×
135

136
        async Task<AuthenticatorResult> IAuthenticator.GetTokenAsync(bool forceRefresh)
137
        {
138
            var watch = Stopwatch.StartNew();
×
139

140
            var result = await Retry.Run(
×
141
                task: () => AcquireTokenAsync(forceRefresh),
×
142
                retryExceptionHandler: (ex, ct) => HandleMsalException(ex, ct)).ConfigureAwait(false);
×
143

144
            watch.Stop();
×
145
            _logger?.LogInformation($"GetTokenAsync: Acquired token using MSAL in {watch.ElapsedMilliseconds}.");
×
146

147
            return result;
×
148
        }
×
149

150
        /// <inheritdoc/>
151
        protected override Lazy<IAuthenticator> BuildIAuthenticator()
152
        {
153
            return new Lazy<IAuthenticator>(() => this, LazyThreadSafetyMode.ExecutionAndPublication);
×
154
        }
155

156
        private async Task<AuthenticatorResult> AcquireTokenAsync(bool forceRefresh = false)
157
        {
158
            if (_clientApplication == null)
×
159
            {
160
                throw new InvalidOperationException("AcquireTokenAsync should not be called for empty credentials.");
×
161
            }
162

163
            bool acquired = false;
×
164

165
            try
166
            {
167
                // Limiting concurrency on MSAL token acquisitions. When the Token is in cache there is never
168
                // contention on this semaphore, but when tokens expire there is some. However, after measuring performance
169
                // with and without the semaphore (and different configs for the semaphore), not limiting concurrency actually
170
                // results in higher response times, more throttling and more contention. 
171
                // Without the use of this semaphore calls to AcquireTokenAsync can take tens of seconds under high concurrency scenarios.
172
#pragma warning disable VSTHRD103 // Call async methods when in an async method
173
                acquired = tokenRefreshSemaphore.Wait(SemaphoreTimeout);
×
174
#pragma warning restore VSTHRD103 // Call async methods when in an async method
175

176
                // If we are allowed to enter the semaphore, acquire the token.
177
                if (acquired)
×
178
                {
179
                    // Note that in MSAL, we dont pass resources anymore, and we instead pass scopes. To be recognized by MSAL, we append the '/.default' to the scope.
180
                    // Scope requirements described in MSAL migration spec: https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-net-migration.
181
                    const string scopePostFix = "/.default";
182
                    var scope = _scope ?? OAuthScope;
×
183

184
                    if (!scope.EndsWith(scopePostFix, StringComparison.OrdinalIgnoreCase))
×
185
                    {
186
                        scope = $"{scope}{scopePostFix}";
×
187
                    }
188

189
                    // Acquire token async using MSAL.NET
190
                    // This will use the cache from the application cache of the MSAL library, no external caching is needed.
191
                    var msalResult = await _clientApplication
×
192
                        .AcquireTokenForClient(new[] { scope })
×
193
                        .WithAuthority(_authority ?? OAuthEndpoint, _validateAuthority)
×
194
                        .WithForceRefresh(forceRefresh)
×
195
                        .ExecuteAsync().ConfigureAwait(false);
×
196

197
                    // This means we acquired a valid token successfully. We can make our retry policy null.
198
                    return new AuthenticatorResult()
×
199
                    {
×
200
                        AccessToken = msalResult.AccessToken,
×
201
                        ExpiresOn = msalResult.ExpiresOn
×
202
                    };
×
203
                }
204
                else
205
                {
206
                    // If the token is taken, it means that one thread is trying to acquire a token from the server.
207
                    // Throttle this request to allow the currently running request to fulfill and then let this one get the result from the cache.
208
                    throw new ThrottleException() { RetryParams = RetryParams.DefaultBackOff(0) };
×
209
                }
210
            }
211
            finally
212
            {
213
                // Always release the semaphore if we acquired it.
214
                if (acquired)
×
215
                {
216
                    ReleaseSemaphore();
×
217
                }
218
            }
219
        }
×
220

221
        private void ReleaseSemaphore()
222
        {
223
            try
224
            {
225
                tokenRefreshSemaphore.Release();
×
226
            }
×
227
            catch (SemaphoreFullException)
×
228
            {
229
                // This should never happen but we want to know if it does.
230
                _logger?.LogWarning("Attempted to release a full semaphore.");
×
231
            }
×
232

233
            // Any exception other than SemaphoreFullException should be thrown right away
234
        }
×
235

236
        private RetryParams HandleMsalException(Exception ex, int ct)
237
        {
238
            _logger?.LogError(ex, "Exception acquiring token through MSAL.");
×
239

240
            if (ex is MsalServiceException msalException)
×
241
            {
242
                _logger?.LogWarning(msalException, $"MSAL service error code: {msalException.ErrorCode}.");
×
243

244
                // Service error with status code "temporarily_unavailable" is retryable.
245
                // Spec and reference: https://docs.microsoft.com/en-us/azure/active-directory/develop/reference-aadsts-error-codes.
246
                if (msalException.ErrorCode == "temporarily_unavailable")
×
247
                {
248
                    return RetryParams.DefaultBackOff(ct);
×
249
                }
250
            }
251

252
            return RetryParams.StopRetrying;
×
253
        }
254
    }
255
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc