Skip to content

Commit eae6c13

Browse files
authored
Probe IMDS /metadata/instance endpoint during discovery in MsiAccessTokenProvider (Azure#14631)
* Call imds instance endpoint instead of token endpoint to probe for imds, extend timeout * Update msi token tests to distinguish between which imds endpoint is being called * Fix comment and visibility in test * Change per stpetrov * re-trigger checks
1 parent 6ae13e6 commit eae6c13

File tree

5 files changed

+92
-50
lines changed

5 files changed

+92
-50
lines changed

sdk/mgmtcommon/AppAuthentication/Azure.Services.AppAuthentication.Unit.Tests/Mocks/MockMsi.cs

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,28 +33,31 @@ internal enum MsiTestType
3333
MsiAppJsonParseFailure,
3434
MsiMissingToken,
3535
MsiAppServicesIncorrectRequest,
36-
MsiAzureVmTimeout,
36+
MsiAzureVmImdsTimeout,
3737
MsiUnresponsive,
3838
MsiThrottled,
3939
MsiTransientServerError
4040
}
4141

4242
private readonly MsiTestType _msiTestType;
4343

44+
private const string _azureVmImdsInstanceEndpoint = "http://169.254.169.254/metadata/instance";
45+
private const string _azureVmImdsTokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token";
46+
4447
internal MockMsi(MsiTestType msiTestType)
4548
{
4649
_msiTestType = msiTestType;
4750
}
4851

4952
/// <summary>
50-
/// Returns a response based on the response type.
53+
/// Returns a response based on the response type.
5154
/// </summary>
5255
/// <param name="request"></param>
5356
/// <param name="cancellationToken"></param>
5457
/// <returns></returns>
5558
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
5659
{
57-
// HitCount is updated when this method gets called. This allows for testing of cache and retry logic.
60+
// HitCount is updated when this method gets called. This allows for testing of cache and retry logic.
5861
HitCount++;
5962

6063
HttpResponseMessage responseMessage = null;
@@ -138,16 +141,20 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
138141
};
139142
break;
140143

141-
case MsiTestType.MsiAzureVmTimeout:
142-
var start = DateTime.Now;
143-
while(DateTime.Now - start < TimeSpan.FromSeconds(MsiAccessTokenProvider.AzureVmImdsProbeTimeoutInSeconds + 10))
144+
case MsiTestType.MsiAzureVmImdsTimeout:
145+
if (request.RequestUri.AbsoluteUri.StartsWith(_azureVmImdsInstanceEndpoint))
144146
{
145-
if (cancellationToken.IsCancellationRequested)
147+
var start = DateTime.Now;
148+
while (DateTime.Now - start < TimeSpan.FromSeconds(MsiAccessTokenProvider.AzureVmImdsProbeTimeoutInSeconds + 10))
146149
{
147-
throw new TaskCanceledException();
150+
if (cancellationToken.IsCancellationRequested)
151+
{
152+
throw new TaskCanceledException();
153+
}
148154
}
155+
throw new Exception("Test fail");
149156
}
150-
throw new Exception("Test fail");
157+
break;
151158

152159
case MsiTestType.MsiUnresponsive:
153160
case MsiTestType.MsiThrottled:
@@ -167,7 +174,20 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
167174
// give error based on test type
168175
if (_msiTestType == MsiTestType.MsiUnresponsive)
169176
{
170-
throw new HttpRequestException();
177+
if (request.RequestUri.AbsoluteUri.StartsWith(_azureVmImdsInstanceEndpoint))
178+
{
179+
responseMessage = new HttpResponseMessage
180+
{
181+
Content = new StringContent(TokenHelper.GetInstanceMetadataResponse(),
182+
Encoding.UTF8,
183+
Constants.JsonContentType)
184+
};
185+
}
186+
else if (Environment.GetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv) != null
187+
|| request.RequestUri.AbsoluteUri.StartsWith(_azureVmImdsTokenEndpoint))
188+
{
189+
throw new HttpRequestException();
190+
}
171191
}
172192
else
173193
{

sdk/mgmtcommon/AppAuthentication/Azure.Services.AppAuthentication.Unit.Tests/MsiAccessTokenProviderTests.cs

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
namespace Microsoft.Azure.Services.AppAuthentication.Unit.Tests
1515
{
1616
/// <summary>
17-
/// Test cases for MsiAccessTokenProvider class. MsiAccessTokenProvider is an internal class.
17+
/// Test cases for MsiAccessTokenProvider class. MsiAccessTokenProvider is an internal class.
1818
/// </summary>
1919
public class MsiAccessTokenProviderTests : IDisposable
2020
{
@@ -48,26 +48,26 @@ public async Task GetTokenUsingManagedIdentityAzureVm(bool specifyUserAssignedMa
4848
expectedAppId = Constants.TestAppId;
4949
}
5050

51-
// MockMsi is being asked to act like response from Azure VM MSI succeeded.
51+
// MockMsi is being asked to act like response from Azure VM MSI succeeded.
5252
MockMsi mockMsi = new MockMsi(msiTestType);
5353
HttpClient httpClient = new HttpClient(mockMsi);
5454
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient, managedIdentityClientId: managedIdentityArgument);
5555

5656
// Get token.
5757
var authResult = await msiAccessTokenProvider.GetAuthResultAsync(Constants.KeyVaultResourceId, Constants.TenantId).ConfigureAwait(false);
5858

59-
// Check if the principalused and type are as expected.
59+
// Check if the principalused and type are as expected.
6060
Validator.ValidateToken(authResult.AccessToken, msiAccessTokenProvider.PrincipalUsed, Constants.AppType, Constants.TenantId, expectedAppId, expiresOn: authResult.ExpiresOn);
6161
}
6262

6363
/// <summary>
64-
/// If json parse error when aquiring token, an exception should be thrown.
64+
/// If json parse error when aquiring token, an exception should be thrown.
6565
/// </summary>
6666
/// <returns></returns>
6767
[Fact]
6868
public async Task ParseErrorMsiGetTokenTest()
6969
{
70-
// MockMsi is being asked to act like response from Azure VM MSI suceeded.
70+
// MockMsi is being asked to act like response from Azure VM MSI suceeded.
7171
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiAppJsonParseFailure);
7272
HttpClient httpClient = new HttpClient(mockMsi);
7373
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient);
@@ -80,13 +80,13 @@ public async Task ParseErrorMsiGetTokenTest()
8080
}
8181

8282
/// <summary>
83-
/// If MSI response if missing the token, an exception should be thrown.
83+
/// If MSI response if missing the token, an exception should be thrown.
8484
/// </summary>
8585
/// <returns></returns>
8686
[Fact]
8787
public async Task MsiResponseMissingTokenTest()
8888
{
89-
// MockMsi is being asked to act like response from Azure VM MSI failed.
89+
// MockMsi is being asked to act like response from Azure VM MSI failed.
9090
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiMissingToken);
9191
HttpClient httpClient = new HttpClient(mockMsi);
9292
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient);
@@ -103,7 +103,7 @@ public async Task MsiResponseMissingTokenTest()
103103
[InlineData(false)]
104104
public async Task GetTokenUsingManagedIdentityAppServices(bool specifyUserAssignedManagedIdentity)
105105
{
106-
// Setup the environment variables that App Service MSI would setup.
106+
// Setup the environment variables that App Service MSI would setup.
107107
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
108108
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
109109

@@ -125,19 +125,19 @@ public async Task GetTokenUsingManagedIdentityAppServices(bool specifyUserAssign
125125
expectedAppId = Constants.TestAppId;
126126
}
127127

128-
// MockMsi is being asked to act like response from App Service MSI suceeded.
128+
// MockMsi is being asked to act like response from App Service MSI suceeded.
129129
MockMsi mockMsi = new MockMsi(msiTestType);
130130
HttpClient httpClient = new HttpClient(mockMsi);
131131
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient, managedIdentityClientId: managedIdentityArgument);
132132

133-
// Get token. This confirms that the environment variables are being read.
133+
// Get token. This confirms that the environment variables are being read.
134134
var authResult = await msiAccessTokenProvider.GetAuthResultAsync(Constants.KeyVaultResourceId, Constants.TenantId).ConfigureAwait(false);
135135

136136
Validator.ValidateToken(authResult.AccessToken, msiAccessTokenProvider.PrincipalUsed, Constants.AppType, Constants.TenantId, expectedAppId, expiresOn: authResult.ExpiresOn);
137137
}
138138

139139
/// <summary>
140-
/// Test response when IDENTITY_HEADER in AppServices MSI is invalid.
140+
/// Test response when IDENTITY_HEADER in AppServices MSI is invalid.
141141
/// </summary>
142142
/// <returns></returns>
143143
[Fact]
@@ -147,7 +147,7 @@ public async Task UnauthorizedTest()
147147
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
148148
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
149149

150-
// MockMsi is being asked to act like response from App Service MSI failed (unauthorized).
150+
// MockMsi is being asked to act like response from App Service MSI failed (unauthorized).
151151
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiAppServicesUnauthorized);
152152
HttpClient httpClient = new HttpClient(mockMsi);
153153
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient);
@@ -159,7 +159,7 @@ public async Task UnauthorizedTest()
159159
}
160160

161161
/// <summary>
162-
/// Test that response when MSI request is not valid is as expected.
162+
/// Test that response when MSI request is not valid is as expected.
163163
/// </summary>
164164
/// <returns></returns>
165165
[Fact]
@@ -180,7 +180,7 @@ public async Task IncorrectFormatTest()
180180
}
181181

182182
/// <summary>
183-
/// If an unexpected http response has been received, ensure exception is thrown.
183+
/// If an unexpected http response has been received, ensure exception is thrown.
184184
/// </summary>
185185
/// <returns></returns>
186186
[Fact]
@@ -208,13 +208,13 @@ public async Task AzureVmImdsTimeoutTest()
208208
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, null);
209209
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, null);
210210

211-
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiAzureVmTimeout);
211+
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiAzureVmImdsTimeout);
212212
HttpClient httpClient = new HttpClient(mockMsi);
213213
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient);
214214

215215
var exception = await Assert.ThrowsAsync<AzureServiceTokenProviderException>(() => Task.Run(() => msiAccessTokenProvider.GetAuthResultAsync(Constants.KeyVaultResourceId, Constants.TenantId)));
216216

217-
Assert.Contains(AzureServiceTokenProviderException.MsiEndpointNotListening, exception.Message);
217+
Assert.Contains(AzureServiceTokenProviderException.MetadataEndpointNotListening, exception.Message);
218218
Assert.DoesNotContain(AzureServiceTokenProviderException.RetryFailure, exception.Message);
219219
}
220220

@@ -224,7 +224,7 @@ public async Task AzureVmImdsTimeoutTest()
224224
[InlineData(MockMsi.MsiTestType.MsiTransientServerError)]
225225
internal async Task TransientErrorRetryTest(MockMsi.MsiTestType testType)
226226
{
227-
// To simplify tests, mock as MSI App Services to skip Azure VM IDMS probe request by
227+
// To simplify tests, mock as MSI App Services to skip Azure VM IDMS probe request by
228228
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
229229
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
230230

@@ -254,12 +254,17 @@ internal async Task TransientErrorRetryTest(MockMsi.MsiTestType testType)
254254
}
255255
}
256256

257-
[Fact]
258-
private async Task MsiRetryTimeoutTest()
257+
[Theory]
258+
[InlineData(false)]
259+
[InlineData(true)]
260+
internal async Task MsiRetryTimeoutTest(bool isAppServices)
259261
{
260-
// To simplify tests, mock as MSI App Services to skip Azure VM IDMS probe request
261-
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
262-
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
262+
if (isAppServices)
263+
{
264+
// Mock as MSI App Services to skip Azure VM IDMS probe request
265+
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
266+
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
267+
}
263268

264269
int timeoutInSeconds = (new Random()).Next(1, 4);
265270

@@ -291,11 +296,11 @@ private async Task AppServicesDifferentCultureTest()
291296
// ensure thread culture is NOT using en-US culture (App Services MSI endpoint always uses en-US DateTime format)
292297
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-GB");
293298

294-
// Setup the environment variables that App Service MSI would setup.
299+
// Setup the environment variables that App Service MSI would setup.
295300
Environment.SetEnvironmentVariable(Constants.MsiAppServiceEndpointEnv, Constants.MsiEndpoint);
296301
Environment.SetEnvironmentVariable(Constants.MsiAppServiceHeaderEnv, Constants.ClientSecret);
297302

298-
// MockMsi is being asked to act like response from App Service MSI suceeded.
303+
// MockMsi is being asked to act like response from App Service MSI suceeded.
299304
MockMsi mockMsi = new MockMsi(MockMsi.MsiTestType.MsiAppServicesSuccess);
300305
HttpClient httpClient = new HttpClient(mockMsi);
301306
MsiAccessTokenProvider msiAccessTokenProvider = new MsiAccessTokenProvider(httpClient);

sdk/mgmtcommon/AppAuthentication/Azure.Services.AppAuthentication.Unit.Tests/TokenHelper.cs

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ namespace Microsoft.Azure.Services.AppAuthentication.Unit.Tests
1212
public class TokenHelper
1313
{
1414
/// <summary>
15-
/// The hardcoded user token has expiry replaced by [exp], so we can replace it with some value to test functionality.
15+
/// The hardcoded user token has expiry replaced by [exp], so we can replace it with some value to test functionality.
1616
/// </summary>
1717
/// <param name="accessToken"></param>
1818
/// <param name="secondsFromCurrent"></param>
@@ -30,7 +30,7 @@ private static string UpdateTokenTime(string accessToken, long secondsFromCurren
3030

3131
internal static string GetUserToken()
3232
{
33-
// Gets a user token that will expire in 10 seconds from now.
33+
// Gets a user token that will expire in 10 seconds from now.
3434
return GetUserToken(10);
3535
}
3636

@@ -63,6 +63,16 @@ internal static string GetUserTokenResponse(long secondsFromCurrent, bool format
6363
return tokenResult;
6464
}
6565

66+
/// <summary>
67+
/// Sample IMDS /instance response
68+
/// </summary>
69+
/// <returns></returns>
70+
internal static string GetInstanceMetadataResponse()
71+
{
72+
return
73+
"{\"compute\":{\"location\":\"westus\",\"name\":\"TestBedVm\",\"resourceGroupName\":\"testbed\",\"subscriptionId\":\"bdd789f3-d9d1-4bea-ac14-30a39ed66d33\"}}";
74+
}
75+
6676
/// <summary>
6777
/// The response has claims as expected from App Service MSI response
6878
/// </summary>
@@ -128,7 +138,7 @@ internal static string GetInvalidMsiTokenResponse()
128138
}
129139

130140
/// <summary>
131-
/// The response has claims as expected from Client Credentials flow response.
141+
/// The response has claims as expected from Client Credentials flow response.
132142
/// </summary>
133143
/// <returns></returns>
134144
internal static string GetAppToken()
@@ -139,7 +149,7 @@ internal static string GetAppToken()
139149
}
140150

141151
/// <summary>
142-
/// Invalid AppToken.
152+
/// Invalid AppToken.
143153
/// </summary>
144154
/// <returns></returns>
145155
internal static string GetInvalidAppToken()

sdk/mgmtcommon/AppAuthentication/Azure.Services.AppAuthentication/AzureServiceTokenProviderException.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
namespace Microsoft.Azure.Services.AppAuthentication
1010
{
1111
/// <summary>
12-
/// Instance of this exception is thrown if access token cannot be acquired.
12+
/// Instance of this exception is thrown if access token cannot be acquired.
1313
/// </summary>
1414
#if FullNetFx || NETSTANDARD2_0
1515
[Serializable]
1616
#endif
1717

1818
public class AzureServiceTokenProviderException : Exception
1919
{
20+
internal const string MetadataEndpointNotListening = "Unable to connect to the Instance Metadata Service (IMDS). Skipping request to the Managed Service Identity (MSI) token endpoint.";
21+
2022
internal const string MsiEndpointNotListening = "Unable to connect to the Managed Service Identity (MSI) endpoint. Please check that you are running on an Azure resource that has MSI setup.";
2123

2224
internal const string UnableToParseMsiTokenResponse = "A successful response was received from Managed Service Identity, but it could not be parsed.";
@@ -42,7 +44,7 @@ public class AzureServiceTokenProviderException : Exception
4244
internal const string NonRetryableError = "Received a non-retryable error.";
4345

4446
/// <summary>
45-
/// Creates an instance of AzureServiceTokenProviderException.
47+
/// Creates an instance of AzureServiceTokenProviderException.
4648
/// </summary>
4749
/// <param name="connectionString">Connection string used.</param>
4850
/// <param name="resource">Resource for which token was expected.</param>

0 commit comments

Comments
 (0)