google-api-dotnet-client / Src /Support /Google.Apis.Auth.Tests /OAuth2 /AwsExternalAccountCredentialTest.cs
| /* | |
| Copyright 2022 Google LLC | |
| Licensed under the Apache License, Version 2.0 (the "License"); | |
| you may not use this file except in compliance with the License. | |
| You may obtain a copy of the License at | |
| https://www.apache.org/licenses/LICENSE-2.0 | |
| Unless required by applicable law or agreed to in writing, software | |
| distributed under the License is distributed on an "AS IS" BASIS, | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| See the License for the specific language governing permissions and | |
| limitations under the License. | |
| */ | |
| using Google.Apis.Auth.OAuth2; | |
| using Google.Apis.Json; | |
| using Google.Apis.Tests.Mocks; | |
| using Newtonsoft.Json; | |
| using System; | |
| using System.Linq; | |
| using System.Net.Http; | |
| using System.Threading.Tasks; | |
| using Xunit; | |
| namespace Google.Apis.Auth.Tests.OAuth2 | |
| { | |
| public class AwsExternalAccountCredentialsTests : ExternalAccountCredentialTestsBase | |
| { | |
| private const string FakeImdsv2Url = "http://169.254.169.254/fake-imds/"; | |
| private const string ImdsV2TokenTtlHeaderName = "X-aws-ec2-metadata-token-ttl-seconds"; | |
| private const string ImdsV2TokenTtlSeconds = "3600"; | |
| private const string FakeImdsV2Token = "fake_imdsv2_token"; | |
| private const string ImdsV2TokenHeaderName = "X-aws-ec2-metadata-token"; | |
| private const string FakeRegionUrl = "http://169.254.169.254/fake-region/"; | |
| private const string MetadataRegion = "us-central-a1"; | |
| private const string Region = "us-central-a"; | |
| private const string FakeSecurityCredentialsUrl = "http://169.254.169.254/fake-security-credentials/"; | |
| private const string FakeSecurityCredentialsRole = "fake_role"; | |
| private const string FakeSecurityCredentialsAccessKeyId = "fake_credentials_key_id"; | |
| private const string FakeSecurityCredentialsSecretAccessKey = "fake_credentials_secret"; | |
| private const string FakeSecurityCredentialsToken = "fake_credentials_token"; | |
| private const string FakeVerificationUrl = "http://iam.{region}.fakeaws.com/?Action=GetCallerIdentity&Version=2011-06-15"; | |
| private const string FakeRegionalizedVerificationUrl = "http://iam.us-central-a.fakeaws.com/?Action=GetCallerIdentity&Version=2011-06-15"; | |
| private const string FakeRegionalizedVerificationHost = "iam.us-central-a.fakeaws.com"; | |
| private const string ServiceName = "iam"; | |
| private static readonly DateTime MockUtcNow = new DateTime(2022, 9, 29, 5, 47, 56, DateTimeKind.Utc); | |
| [] | |
| public async Task UniverseDomain_Default() | |
| { | |
| var credential = new AwsExternalAccountCredential(new AwsExternalAccountCredential.Initializer( | |
| TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl)) as IGoogleCredential; | |
| Assert.Equal(GoogleAuthConsts.DefaultUniverseDomain, credential.GetUniverseDomain()); | |
| Assert.Equal(GoogleAuthConsts.DefaultUniverseDomain, await credential.GetUniverseDomainAsync(default)); | |
| } | |
| [] | |
| public async Task UniverseDomain_Custom() | |
| { | |
| var credential = new AwsExternalAccountCredential(new AwsExternalAccountCredential.Initializer( | |
| TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl) | |
| { | |
| UniverseDomain = FakeUniverseDomain | |
| }) as IGoogleCredential; | |
| Assert.Equal(FakeUniverseDomain, credential.GetUniverseDomain()); | |
| Assert.Equal(FakeUniverseDomain, await credential.GetUniverseDomainAsync(default)); | |
| } | |
| [] | |
| public async Task WithUniverseDomain() | |
| { | |
| var credential = new AwsExternalAccountCredential(new AwsExternalAccountCredential.Initializer( | |
| TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl)) as IGoogleCredential; | |
| var newCredential = credential.WithUniverseDomain(FakeUniverseDomain); | |
| Assert.NotSame(credential, newCredential); | |
| Assert.IsType<AwsExternalAccountCredential>(newCredential); | |
| Assert.Equal(GoogleAuthConsts.DefaultUniverseDomain, credential.GetUniverseDomain()); | |
| Assert.Equal(GoogleAuthConsts.DefaultUniverseDomain, await credential.GetUniverseDomainAsync(default)); | |
| Assert.Equal(FakeUniverseDomain, newCredential.GetUniverseDomain()); | |
| Assert.Equal(FakeUniverseDomain, await newCredential.GetUniverseDomainAsync(default)); | |
| } | |
| [] | |
| [] | |
| [] | |
| [] | |
| [] | |
| [] | |
| [] | |
| public void ValidatesAwsMetadataServerUrls(string imdsV2TokenUrl, string regionUrl, string securityCredentials, string inMessage) | |
| { | |
| var exception = Assert.Throws<InvalidOperationException>(() => new AwsExternalAccountCredential( | |
| new AwsExternalAccountCredential.Initializer(TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl) | |
| { | |
| ClientId = FakeClientId, | |
| ClientSecret = FakeClientSecret, | |
| Scopes = new string[] { FakeScope }, | |
| QuotaProject = FakeQuotaProject, | |
| ImdsV2SessionTokenUrl = imdsV2TokenUrl, | |
| RegionUrl = regionUrl, | |
| SecurityCredentialsUrl = securityCredentials, | |
| })); | |
| Assert.Contains(inMessage, exception.Message); | |
| } | |
| [] | |
| public async Task FetchesAccessToken() | |
| { | |
| var messageHandler = new DelegatedMessageHandler( | |
| ValidateImdsV2TokenRequest, | |
| ValidateRegionRequest, | |
| ValidateRoleRequest, | |
| ValidateSecurityCredentialsRequest, | |
| request => ValidateAccessTokenRequest(request, FakeScope, ValidateSubjectToken)); | |
| var credential = new AwsExternalAccountCredential( | |
| new AwsExternalAccountCredential.Initializer(TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl) | |
| { | |
| HttpClientFactory = new MockHttpClientFactory(messageHandler), | |
| ClientId = FakeClientId, | |
| ClientSecret = FakeClientSecret, | |
| Scopes = new string[] { FakeScope }, | |
| QuotaProject = FakeQuotaProject, | |
| ImdsV2SessionTokenUrl = FakeImdsv2Url, | |
| SecurityCredentialsUrl = FakeSecurityCredentialsUrl, | |
| RegionUrl = FakeRegionUrl, | |
| Clock = new MockClock(MockUtcNow) | |
| }); | |
| var token = await credential.GetAccessTokenWithHeadersForRequestAsync(); | |
| AssertAccessTokenWithHeaders(token); | |
| messageHandler.AssertAllCallsMade(); | |
| } | |
| [] | |
| public async Task FetchesAccessToken_Impersonated() | |
| { | |
| var messageHandler = new DelegatedMessageHandler( | |
| ValidateImdsV2TokenRequest, | |
| ValidateRegionRequest, | |
| ValidateRoleRequest, | |
| ValidateSecurityCredentialsRequest, | |
| request => ValidateAccessTokenRequest(request, ImpersonationScope, ValidateSubjectToken), | |
| ValidateImpersonatedAccessTokenRequest); | |
| var credential = new AwsExternalAccountCredential( | |
| new AwsExternalAccountCredential.Initializer(TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl) | |
| { | |
| HttpClientFactory = new MockHttpClientFactory(messageHandler), | |
| ClientId = FakeClientId, | |
| ClientSecret = FakeClientSecret, | |
| Scopes = new string[] { FakeScope }, | |
| QuotaProject = FakeQuotaProject, | |
| ImdsV2SessionTokenUrl = FakeImdsv2Url, | |
| SecurityCredentialsUrl = FakeSecurityCredentialsUrl, | |
| RegionUrl = FakeRegionUrl, | |
| Clock = new MockClock(MockUtcNow), | |
| ServiceAccountImpersonationUrl = ImpersonationUrl | |
| }); | |
| var token = await credential.GetAccessTokenWithHeadersForRequestAsync(); | |
| AssertImpersonatedAccessTokenWithHeaders(token); | |
| messageHandler.AssertAllCallsMade(); | |
| } | |
| [] | |
| public async Task RefreshesAccessToken() | |
| { | |
| var messageHandler = new DelegatedMessageHandler( | |
| ValidateImdsV2TokenRequest, | |
| ValidateRegionRequest, | |
| ValidateRoleRequest, | |
| ValidateSecurityCredentialsRequest, | |
| AccessTokenRequest, | |
| ValidateImdsV2TokenRequest, | |
| ValidateRegionRequest, | |
| ValidateRoleRequest, | |
| ValidateSecurityCredentialsRequest, | |
| RefreshTokenRequest); | |
| var clock = new MockClock(MockUtcNow); | |
| var credential = new AwsExternalAccountCredential( | |
| new AwsExternalAccountCredential.Initializer(TokenUrl, FakeAudience, FakeSubjectTokenType, FakeVerificationUrl) | |
| { | |
| HttpClientFactory = new MockHttpClientFactory(messageHandler), | |
| ClientId = FakeClientId, | |
| ClientSecret = FakeClientSecret, | |
| Scopes = new string[] { FakeScope }, | |
| QuotaProject = FakeQuotaProject, | |
| ImdsV2SessionTokenUrl = FakeImdsv2Url, | |
| SecurityCredentialsUrl = FakeSecurityCredentialsUrl, | |
| RegionUrl = FakeRegionUrl, | |
| Clock = clock | |
| }); | |
| Assert.Equal(FakeAccessToken, await credential.GetAccessTokenForRequestAsync()); | |
| clock.UtcNow = clock.UtcNow.AddDays(2); | |
| Assert.Equal(FakeRefreshedAccessToken, await credential.GetAccessTokenForRequestAsync()); | |
| messageHandler.AssertAllCallsMade(); | |
| } | |
| private static Task<HttpResponseMessage> ValidateImdsV2TokenRequest(HttpRequestMessage imdsV2Request) | |
| { | |
| Assert.Equal(FakeImdsv2Url, imdsV2Request.RequestUri.ToString()); | |
| Assert.Equal(HttpMethod.Put, imdsV2Request.Method); | |
| Assert.Contains(imdsV2Request.Headers, header => header.Key == ImdsV2TokenTtlHeaderName && header.Value.Single() == ImdsV2TokenTtlSeconds); | |
| return BuildStringContentResponse(FakeImdsV2Token); | |
| } | |
| private static Task<HttpResponseMessage> ValidateRegionRequest(HttpRequestMessage regionRequest) | |
| { | |
| Assert.Equal(FakeRegionUrl, regionRequest.RequestUri.ToString()); | |
| Assert.Equal(HttpMethod.Get, regionRequest.Method); | |
| Assert.Contains(regionRequest.Headers, header => header.Key == ImdsV2TokenHeaderName && header.Value.Single() == FakeImdsV2Token); | |
| return BuildStringContentResponse(MetadataRegion); | |
| } | |
| private static Task<HttpResponseMessage> ValidateRoleRequest(HttpRequestMessage roleRequest) | |
| { | |
| Assert.Equal(FakeSecurityCredentialsUrl, roleRequest.RequestUri.ToString()); | |
| Assert.Equal(HttpMethod.Get, roleRequest.Method); | |
| Assert.Contains(roleRequest.Headers, header => header.Key == ImdsV2TokenHeaderName && header.Value.Single() == FakeImdsV2Token); | |
| return BuildStringContentResponse(FakeSecurityCredentialsRole); | |
| } | |
| private static Task<HttpResponseMessage> ValidateSecurityCredentialsRequest(HttpRequestMessage roleRequest) | |
| { | |
| Assert.Equal($"{FakeSecurityCredentialsUrl}{FakeSecurityCredentialsRole}", roleRequest.RequestUri.ToString()); | |
| Assert.Equal(HttpMethod.Get, roleRequest.Method); | |
| Assert.Contains(roleRequest.Headers, header => header.Key == ImdsV2TokenHeaderName && header.Value.Single() == FakeImdsV2Token); | |
| return BuildStringContentResponseFromJson( | |
| new | |
| { | |
| Code = "Success", | |
| AccessKeyId = FakeSecurityCredentialsAccessKeyId, | |
| SecretAccessKey = FakeSecurityCredentialsSecretAccessKey, | |
| Token = FakeSecurityCredentialsToken | |
| }); | |
| } | |
| private static void ValidateSubjectToken(string accessTokenRequestContent) | |
| { | |
| int start = accessTokenRequestContent.IndexOf("subject_token=", StringComparison.Ordinal) + 14; | |
| int end = accessTokenRequestContent.IndexOf("&subject_token_type=", StringComparison.Ordinal); | |
| string subjectToken = Uri.UnescapeDataString(accessTokenRequestContent.Substring(start, end - start)); | |
| var deserializedSubjectToken = NewtonsoftJsonSerializer.Instance.Deserialize<AwsSignedSubjectToken>(subjectToken); | |
| Assert.Equal(FakeRegionalizedVerificationUrl, deserializedSubjectToken.Url); | |
| Assert.Equal("POST", deserializedSubjectToken.HttpMethod); | |
| Assert.Equal("", deserializedSubjectToken.Body); | |
| Assert.Contains(deserializedSubjectToken.Headers, header => header.Key == "x-goog-cloud-target-resource" && header.Value == FakeAudience); | |
| Assert.Contains(deserializedSubjectToken.Headers, header => header.Key == "x-amz-date" && header.Value == "20220929T054756Z"); | |
| Assert.Contains(deserializedSubjectToken.Headers, header => header.Key == "host" && header.Value == FakeRegionalizedVerificationHost); | |
| Assert.Contains(deserializedSubjectToken.Headers, header => header.Key == "x-amz-security-token" && header.Value == FakeSecurityCredentialsToken); | |
| var authorizationHeaderValue = Assert.Single(deserializedSubjectToken.Headers, header => header.Key == "Authorization").Value; | |
| Assert.Contains("AWS4-HMAC-SHA256", authorizationHeaderValue); | |
| Assert.Contains("/20220929/", authorizationHeaderValue); | |
| Assert.Contains($"/{Region}/", authorizationHeaderValue); | |
| Assert.Contains($"/{ServiceName}/", authorizationHeaderValue); | |
| Assert.Contains($"/{ServiceName}/", authorizationHeaderValue); | |
| Assert.Contains($"host;x-amz-date;x-amz-security-token;x-goog-cloud-target-resource", authorizationHeaderValue); | |
| } | |
| public class AwsSignedSubjectToken | |
| { | |
| [] | |
| public string Url { get; set; } | |
| [] | |
| public string HttpMethod { get; set; } | |
| [] | |
| public string Body { get; set; } | |
| [] | |
| public AwsSignedSubjectTokenHeader[] Headers { get; set; } | |
| public class AwsSignedSubjectTokenHeader | |
| { | |
| [] | |
| public string Key { get; set; } | |
| [] | |
| public string Value { get; set; } | |
| } | |
| } | |
| } | |
| } |