-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
Copy pathAzureServiceTokenProviderFactory.cs
340 lines (307 loc) · 18.4 KB
/
AzureServiceTokenProviderFactory.cs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.Security.Cryptography.X509Certificates;
using System.Text.RegularExpressions;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
namespace Microsoft.Azure.Services.AppAuthentication
{
/// <summary>
/// Creates an access token provider based on the connection string.
/// </summary>
internal class AzureServiceTokenProviderFactory
{
private const string RunAs = "RunAs";
private const string Developer = "Developer";
private const string AzureCli = "AzureCLI";
private const string VisualStudio = "VisualStudio";
private const string DeveloperTool = "DeveloperTool";
private const string CurrentUser = "CurrentUser";
private const string App = "App";
private const string AppId = "AppId";
private const string AppKey = "AppKey";
private const string TenantId = "TenantId";
private const string CertificateSubjectName = "CertificateSubjectName";
private const string CertificateThumbprint = "CertificateThumbprint";
private const string KeyVaultCertificateSecretIdentifier = "KeyVaultCertificateSecretIdentifier";
private const string KeyVaultUserAssignedManagedIdentityId = "KeyVaultUserAssignedManagedIdentityId";
private const string CertificateStoreLocation = "CertificateStoreLocation";
private const string MsiRetryTimeout = "MsiRetryTimeout";
// taken from https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Data/Common/DbConnectionOptions.Common.cs
private const string ConnectionStringPattern = // may not contain embedded null except trailing last value
"([\\s;]*" // leading whitespace and extra semicolons
+ "(?![\\s;])" // key does not start with space or semicolon
+ "(?<key>([^=\\s\\p{Cc}]|\\s+[^=\\s\\p{Cc}]|\\s+==|==)+)" // allow any visible character for keyname except '=' which must quoted as '=='
+ "\\s*=(?!=)\\s*" // the equal sign divides the key and value parts
+ "(?<value>"
+ "(\"([^\"\u0000]|\"\")*\")" // double quoted string, " must be quoted as ""
+ "|"
+ "('([^'\u0000]|'')*')" // single quoted string, ' must be quoted as ''
+ "|"
+ "((?![\"'\\s])" // unquoted value must not start with " or ' or space, would also like = but too late to change
+ "([^;\\s\\p{Cc}]|\\s+[^;\\s\\p{Cc}])*" // control characters must be quoted
+ "(?<![\"']))" // unquoted value must not stop with " or '
+ ")(\\s*)(;|[\u0000\\s]*$)" // whitespace after value up to semicolon or end-of-line
+ ")*" // repeat the key-value pair
+ "[\\s;]*[\u0000\\s]*" // trailing whitespace/semicolons (DataSourceLocator), embedded nulls are allowed only in the end
;
private static readonly Regex ConnectionStringRegex = new Regex(ConnectionStringPattern, RegexOptions.ExplicitCapture | RegexOptions.Compiled);
/// <summary>
/// Returns a specific token provider based on authentication option specified in the connection string.
/// </summary>
/// <param name="connectionString">Connection string with authentication option and related parameters.</param>
/// <param name="azureAdInstance"></param>
/// <returns></returns>
internal static NonInteractiveAzureServiceTokenProviderBase Create(string connectionString, string azureAdInstance, IHttpClientFactory httpClientFactory = default)
{
Dictionary<string, string> connectionSettings = ParseConnectionString(connectionString);
NonInteractiveAzureServiceTokenProviderBase azureServiceTokenProvider;
ValidateAttribute(connectionSettings, RunAs, connectionString);
string runAs = connectionSettings[RunAs];
if (string.Equals(runAs, Developer, StringComparison.OrdinalIgnoreCase))
{
// If RunAs=Developer
ValidateAttribute(connectionSettings, DeveloperTool, connectionString);
// And Dev Tool equals AzureCLI or VisualStudio
if (string.Equals(connectionSettings[DeveloperTool], AzureCli,
StringComparison.OrdinalIgnoreCase))
{
azureServiceTokenProvider = new AzureCliAccessTokenProvider(new ProcessManager());
}
else if (string.Equals(connectionSettings[DeveloperTool], VisualStudio,
StringComparison.OrdinalIgnoreCase))
{
azureServiceTokenProvider = new VisualStudioAccessTokenProvider(new ProcessManager());
}
else
{
throw new ArgumentException($"Connection string {connectionString} is not valid. {DeveloperTool} '{connectionSettings[DeveloperTool]}' is not valid. " +
$"Allowed values are {AzureCli} or {VisualStudio}");
}
}
else if (string.Equals(runAs, CurrentUser, StringComparison.OrdinalIgnoreCase))
{
// If RunAs=CurrentUser
#if FullNetFx
azureServiceTokenProvider = new WindowsAuthenticationAzureServiceTokenProvider(new AdalAuthenticationContext(httpClientFactory), azureAdInstance);
#else
throw new ArgumentException($"Connection string {connectionString} is not supported for .NET Core.");
#endif
}
else if (string.Equals(runAs, App, StringComparison.OrdinalIgnoreCase))
{
// If RunAs=App
// If AppId key is present, use certificate, client secret, or MSI (with user assigned identity) based token provider
if (connectionSettings.ContainsKey(AppId))
{
ValidateAttribute(connectionSettings, AppId, connectionString);
if (connectionSettings.ContainsKey(CertificateStoreLocation))
{
ValidateAttributes(connectionSettings, new List<string> { CertificateSubjectName, CertificateThumbprint }, connectionString);
ValidateAttribute(connectionSettings, CertificateStoreLocation, connectionString);
ValidateStoreLocation(connectionSettings, connectionString);
ValidateAttribute(connectionSettings, TenantId, connectionString);
azureServiceTokenProvider =
new ClientCertificateAzureServiceTokenProvider(
connectionSettings[AppId],
connectionSettings.ContainsKey(CertificateThumbprint)
? connectionSettings[CertificateThumbprint]
: connectionSettings[CertificateSubjectName],
connectionSettings.ContainsKey(CertificateThumbprint)
? ClientCertificateAzureServiceTokenProvider.CertificateIdentifierType.Thumbprint
: ClientCertificateAzureServiceTokenProvider.CertificateIdentifierType.SubjectName,
connectionSettings[CertificateStoreLocation],
azureAdInstance,
connectionSettings[TenantId],
0,
authenticationContext: new AdalAuthenticationContext(httpClientFactory));
}
else if (connectionSettings.ContainsKey(CertificateThumbprint) ||
connectionSettings.ContainsKey(CertificateSubjectName))
{
// if certificate thumbprint or subject name are specified but certificate store location is not, throw error
throw new ArgumentException($"Connection string {connectionString} is not valid. Must contain '{CertificateStoreLocation}' attribute and it must not be empty " +
$"when using '{CertificateThumbprint}' and '{CertificateSubjectName}' attributes");
}
else if (connectionSettings.ContainsKey(KeyVaultCertificateSecretIdentifier))
{
ValidateMsiRetryTimeout(connectionSettings, connectionString);
var msiRetryTimeout = connectionSettings.ContainsKey(MsiRetryTimeout)
? int.Parse(connectionSettings[MsiRetryTimeout])
: 0;
connectionSettings.TryGetValue(KeyVaultUserAssignedManagedIdentityId, out var keyVaultUserAssignedManagedIdentityId);
azureServiceTokenProvider =
new ClientCertificateAzureServiceTokenProvider(
connectionSettings[AppId],
connectionSettings[KeyVaultCertificateSecretIdentifier],
ClientCertificateAzureServiceTokenProvider.CertificateIdentifierType.KeyVaultCertificateSecretIdentifier,
null, // storeLocation unused
azureAdInstance,
connectionSettings.ContainsKey(TenantId) // tenantId can be specified in connection string or retrieved from Key Vault access token later
? connectionSettings[TenantId]
: default,
msiRetryTimeout,
keyVaultUserAssignedManagedIdentityId,
new AdalAuthenticationContext(httpClientFactory));
}
else if (connectionSettings.ContainsKey(AppKey))
{
ValidateAttribute(connectionSettings, AppKey, connectionString);
ValidateAttribute(connectionSettings, TenantId, connectionString);
azureServiceTokenProvider =
new ClientSecretAccessTokenProvider(
connectionSettings[AppId],
connectionSettings[AppKey],
connectionSettings[TenantId],
azureAdInstance,
new AdalAuthenticationContext(httpClientFactory));
}
else
{
ValidateMsiRetryTimeout(connectionSettings, connectionString);
// If certificate or client secret are not specified, use the specified managed identity
azureServiceTokenProvider = new MsiAccessTokenProvider(
connectionSettings.ContainsKey(MsiRetryTimeout)
? int.Parse(connectionSettings[MsiRetryTimeout])
: 0,
connectionSettings[AppId]);
}
}
else
{
ValidateMsiRetryTimeout(connectionSettings, connectionString);
// If AppId is not specified, use Managed Service Identity
azureServiceTokenProvider = new MsiAccessTokenProvider(
connectionSettings.ContainsKey(MsiRetryTimeout)
? int.Parse(connectionSettings[MsiRetryTimeout])
: 0);
}
}
else
{
throw new ArgumentException($"Connection string {connectionString} is not valid. RunAs value '{connectionSettings[RunAs]}' is not valid. " +
$"Allowed values are {Developer}, {CurrentUser}, or {App}");
}
azureServiceTokenProvider.ConnectionString = connectionString;
return azureServiceTokenProvider;
}
private static void ValidateAttribute(Dictionary<string, string> connectionSettings, string attribute,
string connectionString)
{
if (connectionSettings != null &&
(!connectionSettings.ContainsKey(attribute) || string.IsNullOrWhiteSpace(connectionSettings[attribute])))
{
throw new ArgumentException($"Connection string {connectionString} is not valid. Must contain '{attribute}' attribute and it must not be empty.");
}
}
/// <summary>
/// Throws an exception if none of the attributes are in the connection string
/// </summary>
/// <param name="connectionSettings">List of key value pairs in the connection string</param>
/// <param name="attributes">List of attributes to test</param>
/// <param name="connectionString">The connection string specified</param>
private static void ValidateAttributes(Dictionary<string, string> connectionSettings, List<string> attributes,
string connectionString)
{
if (connectionSettings != null)
{
foreach (string attribute in attributes)
{
if (connectionSettings.ContainsKey(attribute))
{
return;
}
}
throw new ArgumentException($"Connection string {connectionString} is not valid. Must contain at least one of {string.Join(" or ", attributes)} attributes.");
}
}
private static void ValidateStoreLocation(Dictionary<string, string> connectionSettings, string connectionString)
{
if (connectionSettings != null && connectionSettings.ContainsKey(CertificateStoreLocation))
{
if (!string.IsNullOrEmpty(connectionSettings[CertificateStoreLocation]))
{
StoreLocation location;
string storeLocation = connectionSettings[CertificateStoreLocation];
bool parseSucceeded = Enum.TryParse(storeLocation, true, out location);
if (!parseSucceeded)
{
throw new ArgumentException(
$"Connection string {connectionString} is not valid. StoreLocation {storeLocation} is not valid. Valid values are CurrentUser and LocalMachine.");
}
}
}
}
private static void ValidateMsiRetryTimeout(Dictionary<string, string> connectionSettings, string connectionString)
{
if (connectionSettings != null && connectionSettings.ContainsKey(MsiRetryTimeout))
{
if (!string.IsNullOrEmpty(connectionSettings[MsiRetryTimeout]))
{
int timeoutInt;
string timeoutString = connectionSettings[MsiRetryTimeout];
bool parseSucceeded = int.TryParse(timeoutString, out timeoutInt);
if (!parseSucceeded)
{
throw new ArgumentException(
$"Connection string {connectionString} is not valid. MsiRetryTimeout {timeoutString} is not valid. Valid values are integers greater than or equal to 0.");
}
}
}
}
// adapted from https://github.com/dotnet/corefx/blob/master/src/Common/src/System/Data/Common/DbConnectionOptions.Common.cs
internal static Dictionary<string, string> ParseConnectionString(string connectionString)
{
Dictionary<string, string> connectionSettings = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
const int KeyIndex = 1, ValueIndex = 2;
if (!string.IsNullOrWhiteSpace(connectionString))
{
Match match = ConnectionStringRegex.Match(connectionString);
if (!match.Success || (match.Length != connectionString.Length))
{
throw new ArgumentException(
$"Connection string {connectionString} is not in a proper format. Expected format is Key1=Value1;Key2=Value2;");
}
int indexValue = 0;
CaptureCollection keyValues = match.Groups[ValueIndex].Captures;
foreach (Capture keyNames in match.Groups[KeyIndex].Captures)
{
string key = keyNames.Value.Replace("==", "=");
string value = keyValues[indexValue++].Value;
if (value.Length > 0)
{
switch (value[0])
{
case '\"':
value = value.Substring(1, value.Length - 2).Replace("\"\"", "\"");
break;
case '\'':
value = value.Substring(1, value.Length - 2).Replace("\'\'", "\'");
break;
default:
break;
}
}
if (!string.IsNullOrWhiteSpace(key))
{
if (!connectionSettings.ContainsKey(key))
{
connectionSettings[key] = value;
}
else
{
throw new ArgumentException(
$"Connection string {connectionString} is not in a proper format. Key '{key}' is repeated.");
}
}
}
}
else
{
throw new ArgumentException("Connection string is empty.");
}
return connectionSettings;
}
}
}