-
Notifications
You must be signed in to change notification settings - Fork 4.9k
/
Copy pathManagedIdentitySource.cs
206 lines (183 loc) · 8.39 KB
/
ManagedIdentitySource.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
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
namespace Azure.Identity
{
internal abstract class ManagedIdentitySource
{
internal const string AuthenticationResponseInvalidFormatError = "Invalid response, the authentication response was not in the expected format.";
internal const string UnexpectedResponse = "Managed Identity response was not in the expected format. See the inner exception for details.";
private ManagedIdentityResponseClassifier _responseClassifier;
protected ManagedIdentitySource(CredentialPipeline pipeline)
{
Pipeline = pipeline;
_responseClassifier = new ManagedIdentityResponseClassifier();
}
protected internal CredentialPipeline Pipeline { get; }
protected internal string ClientId { get; }
public virtual async ValueTask<AccessToken> AuthenticateAsync(bool async, TokenRequestContext context, CancellationToken cancellationToken)
{
using HttpMessage message = CreateHttpMessage(CreateRequest(context.Scopes));
if (async)
{
await Pipeline.HttpPipeline.SendAsync(message, cancellationToken).ConfigureAwait(false);
}
else
{
Pipeline.HttpPipeline.Send(message, cancellationToken);
}
return await HandleResponseAsync(async, context, message, cancellationToken).ConfigureAwait(false);
}
protected virtual async ValueTask<AccessToken> HandleResponseAsync(
bool async,
TokenRequestContext context,
HttpMessage message,
CancellationToken cancellationToken)
{
Exception exception = null;
Response response = message.Response;
try
{
if (response.Status == 200)
{
// This avoids the json parsing if we have already been cancelled.
// Also, this handles the sync case, where we don't have to check for cancellation.
if (cancellationToken.IsCancellationRequested)
{
throw new TaskCanceledException();
}
using JsonDocument json = async
? await JsonDocument.ParseAsync(response.ContentStream, default, cancellationToken).ConfigureAwait(false)
: JsonDocument.Parse(response.ContentStream);
return GetTokenFromResponse(json.RootElement);
}
}
catch (JsonException jex)
{
throw new CredentialUnavailableException(UnexpectedResponse, jex);
}
catch (Exception e) when (response.Status == 200)
{
// This is a rare case where the request times out but the response was successful.
throw new RequestFailedException("Response from Managed Identity was successful, but the operation timed out prior to completion.", e);
}
catch (Exception e)
{
exception = e;
}
//This is a special case for Docker Desktop which responds with a 403 with a message that contains "A socket operation was attempted to an unreachable network/host"
// rather than just timing out, as expected.
// This case can also be hit when some service other than IMDS responds with a non-JSON response.
// In all such cases, we should treat the response as CredentialUnavailable.
if (response.IsError)
{
string content = string.Empty;
try
{
content = response.Content.ToString();
using JsonDocument json = async
? await JsonDocument.ParseAsync(response.ContentStream, default, cancellationToken).ConfigureAwait(false)
: JsonDocument.Parse(response.ContentStream);
}
catch (Exception)
{
// If the response is not json or the Content was null, it is not the IMDS and it should be treated as CredentialUnavailable
throw new CredentialUnavailableException(UnexpectedResponse, new Exception(content));
}
}
throw new RequestFailedException(response, exception);
}
protected abstract Request CreateRequest(string[] scopes);
protected virtual HttpMessage CreateHttpMessage(Request request)
{
return new HttpMessage(request, _responseClassifier);
}
internal static async Task<string> GetMessageFromResponse(Response response, bool async, CancellationToken cancellationToken)
{
if (response?.ContentStream == null || !response.ContentStream.CanRead || response.ContentStream.Length == 0)
{
return null;
}
try
{
response.ContentStream.Position = 0;
using JsonDocument json = async
? await JsonDocument.ParseAsync(response.ContentStream, default, cancellationToken).ConfigureAwait(false)
: JsonDocument.Parse(response.ContentStream);
return GetMessageFromResponse(json.RootElement);
}
catch // parsing failed
{
return "Response was not in a valid json format.";
}
}
protected static string GetMessageFromResponse(in JsonElement root)
{
// Parse the error, if possible
foreach (var prop in root.EnumerateObject())
{
if (prop.Name == "Message")
{
return prop.Value.GetString();
}
}
return null;
}
private static AccessToken GetTokenFromResponse(in JsonElement root)
{
string accessToken = null;
DateTimeOffset? expiresOn = null;
foreach (JsonProperty prop in root.EnumerateObject())
{
switch (prop.Name)
{
case "access_token":
accessToken = prop.Value.GetString();
break;
case "expires_on":
expiresOn = TryParseExpiresOn(prop.Value);
break;
}
}
if (accessToken != null && expiresOn.HasValue)
{
return new AccessToken(accessToken, expiresOn.Value, InferManagedIdentityRefreshInValue(expiresOn.Value));
}
else
{
throw new AuthenticationFailedException(AuthenticationResponseInvalidFormatError);
}
}
private static DateTimeOffset? TryParseExpiresOn(JsonElement jsonExpiresOn)
{
// first test if expiresOn is a unix timestamp either as a number or string
if (jsonExpiresOn.ValueKind == JsonValueKind.Number && jsonExpiresOn.TryGetInt64(out long expiresOnSec) ||
jsonExpiresOn.ValueKind == JsonValueKind.String && long.TryParse(jsonExpiresOn.GetString(), out expiresOnSec))
{
return DateTimeOffset.FromUnixTimeSeconds(expiresOnSec);
}
// otherwise if it is a json string try to parse as a datetime offset
else if (jsonExpiresOn.ValueKind == JsonValueKind.String && DateTimeOffset.TryParse(jsonExpiresOn.GetString(), CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTimeOffset expiresOn))
{
return expiresOn;
}
return null;
}
// Compute refresh_in as 1/2 expiresOn, but only if expiresOn > 2h.
internal static DateTimeOffset? InferManagedIdentityRefreshInValue(DateTimeOffset expiresOn)
{
if (expiresOn > DateTimeOffset.UtcNow.AddHours(2) && expiresOn < DateTimeOffset.MaxValue)
{
// return the midpoint between now and expiresOn
return expiresOn.AddTicks(-(expiresOn.Ticks - DateTimeOffset.UtcNow.Ticks) / 2);
}
return null;
}
}
}