/* * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. * * Licensed under the Apache License, Version 2.0 (the "License"). * You may not use this file except in compliance with the License. * A copy of the License is located at * * http://aws.amazon.com/apache2.0 * * or in the "license" file accompanying this file. This file 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 Amazon.Runtime.Internal; using Amazon.Runtime.Internal.Util; using Amazon.Runtime.SharedInterfaces; using Amazon.Util; using System; using System.Globalization; using System.IO; using System.Net; using System.Text.RegularExpressions; #if AWS_ASYNC_API using System.Threading.Tasks; #endif namespace Amazon.Runtime { /// /// AWS Credentials that automatically refresh by calling AssumeRole on /// the Amazon Security Token Service. /// public class AssumeRoleWithWebIdentityCredentials : RefreshingAWSCredentials { private const int PREEMPT_EXPIRY_MINUTES = 5; private static readonly RegionEndpoint _defaultSTSClientRegion = RegionEndpoint.USEast1; private static readonly string _roleSessionNameDefault = Guid.NewGuid().ToString(); /// /// As established by STS, the regex used to validate the role session names is a string of 2-64 characters consisting of /// upper- and lower-case alphanumeric characters with no spaces. You can also include /// underscores or any of the following characters: =,.@- /// public const string WebIdentityTokenFileEnvVariable = "AWS_WEB_IDENTITY_TOKEN_FILE"; public const string RoleArnEnvVariable = "AWS_ROLE_ARN"; public const string RoleSessionNameEnvVariable = "AWS_ROLE_SESSION_NAME"; private static readonly Regex _roleSessionNameRegex = new Regex(@"^[\w+=,.@-]{2,64}$", RegexOptions.Compiled); private readonly Logger _logger = Logger.GetLogger(typeof(AssumeRoleWithWebIdentityCredentials)); /// /// Options to be used in the call to AssumeRole. /// private AssumeRoleWithWebIdentityCredentialsOptions _options; #region Properties /// /// The absolute path to the file on disk containing an OIDC token /// public string WebIdentityTokenFile { get; } /// /// The Amazon Resource Name (ARN) of the role to assume. /// public string RoleArn { get; } /// /// An identifier for the assumed role session. /// public string RoleSessionName { get; } #endregion Properties /// /// Constructs an AssumeRoleWithWebIdentityCredentials object. /// /// The absolute path to the file on disk containing an OIDC token. /// The Amazon Resource Name (ARN) of the role to assume. /// An identifier for the assumed role session. public AssumeRoleWithWebIdentityCredentials(string webIdentityTokenFile, string roleArn, string roleSessionName) : this(webIdentityTokenFile, roleArn, roleSessionName, new AssumeRoleWithWebIdentityCredentialsOptions()) { } /// /// Constructs an AssumeRoleWithWebIdentityCredentials object. /// /// The absolute path to the file on disk containing an OIDC token. /// The Amazon Resource Name (ARN) of the role to assume. /// An identifier for the assumed role session. /// Options to be used in the call to AssumeRole. public AssumeRoleWithWebIdentityCredentials(string webIdentityTokenFile, string roleArn, string roleSessionName, AssumeRoleWithWebIdentityCredentialsOptions options) { if (string.IsNullOrEmpty(webIdentityTokenFile)) { throw new ArgumentNullException(nameof(webIdentityTokenFile), $"The {nameof(webIdentityTokenFile)} must be an absolute path."); } else if (!AWSSDKUtils.IsAbsolutePath(webIdentityTokenFile)) { throw new ArgumentException($"The {nameof(webIdentityTokenFile)} must be an absolute path.", nameof(webIdentityTokenFile)); } if (string.IsNullOrEmpty(roleArn)) { throw new ArgumentNullException(nameof(roleArn), "The role ARN must be specified."); } if (!string.IsNullOrEmpty(roleSessionName) && !_roleSessionNameRegex.IsMatch(roleSessionName)) { throw new ArgumentOutOfRangeException(nameof(roleSessionName), roleSessionName, $"The value must match the regex pattern @\"{_roleSessionNameRegex}\"."); } WebIdentityTokenFile = webIdentityTokenFile; RoleArn = roleArn; RoleSessionName = string.IsNullOrEmpty(roleSessionName) ? _roleSessionNameDefault : roleSessionName; _options = options; // Make sure to fetch new credentials well before the current credentials expire to avoid // any request being made with expired credentials. PreemptExpiryTime = TimeSpan.FromMinutes(PREEMPT_EXPIRY_MINUTES); } /// /// Creates an instance of from environment variables. /// /// Throws an if the needed environment variables are not set. /// The new credentials. public static AssumeRoleWithWebIdentityCredentials FromEnvironmentVariables() { var webIdentityTokenFile = Environment.GetEnvironmentVariable(WebIdentityTokenFileEnvVariable); var roleArn = Environment.GetEnvironmentVariable(RoleArnEnvVariable); var roleSessionName = Environment.GetEnvironmentVariable(RoleSessionNameEnvVariable); return new AssumeRoleWithWebIdentityCredentials(webIdentityTokenFile, roleArn, roleSessionName); } protected override CredentialsRefreshState GenerateNewCredentials() { string token = null; for (var retry = 0; retry <= AWSSDKUtils.DefaultMaxRetry; retry++) { try { using (var fileStream = new FileStream(WebIdentityTokenFile, FileMode.Open, FileAccess.Read)) // Using FileStream to support NetStandard 1.3 { using (var streamReader = new StreamReader(fileStream)) { token = streamReader.ReadToEnd(); } } break; } catch (Exception e) { if (retry == AWSSDKUtils.DefaultMaxRetry) { _logger.Debug(e, $"A token could not be loaded from the WebIdentityTokenFile at {WebIdentityTokenFile}."); throw new InvalidOperationException("A token could not be loaded from the WebIdentityTokenFile.", e); } DefaultRetryPolicy.WaitBeforeRetry(retry, 1000); } } AssumeRoleImmutableCredentials credentials; using (var coreStsClient = CreateClient()) { credentials = coreStsClient.CredentialsFromAssumeRoleWithWebIdentityAuthentication(token, RoleArn, RoleSessionName, _options); // Will retry InvalidIdentityToken and IDPCommunicationError } _logger.InfoFormat("New credentials created using assume role with web identity that expire at {0}", credentials.Expiration.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture)); return new CredentialsRefreshState(credentials, credentials.Expiration); } #if AWS_ASYNC_API protected override async Task GenerateNewCredentialsAsync() { string token = null; for (var retry = 0; retry <= AWSSDKUtils.DefaultMaxRetry; retry++) { try { using (var fileStream = new FileStream(WebIdentityTokenFile, FileMode.Open, FileAccess.Read)) // Using FileStream to support NetStandard 1.3 { using (var streamReader = new StreamReader(fileStream)) { token = await streamReader.ReadToEndAsync().ConfigureAwait(false); } } break; } catch (Exception e) { if (retry == AWSSDKUtils.DefaultMaxRetry) { _logger.Debug(e, $"A token could not be loaded from the WebIdentityTokenFile at {WebIdentityTokenFile}."); throw new InvalidOperationException("A token could not be loaded from the WebIdentityTokenFile.", e); } DefaultRetryPolicy.WaitBeforeRetry(retry, 1000); } } AssumeRoleImmutableCredentials credentials; using (var coreStsClient = CreateClient()) { credentials = await coreStsClient.CredentialsFromAssumeRoleWithWebIdentityAuthenticationAsync(token, RoleArn, RoleSessionName, _options).ConfigureAwait(false); // Will retry InvalidIdentityToken and IDPCommunicationError } _logger.InfoFormat("New credentials created using assume role with web identity that expire at {0}", credentials.Expiration.ToString("yyyy-MM-ddTHH:mm:ss.fffffffK", CultureInfo.InvariantCulture)); return new CredentialsRefreshState(credentials, credentials.Expiration); } #endif /// /// Gets a client to be used for AssumeRoleWithWebIdentity requests. /// /// The STS client. protected virtual ICoreAmazonSTS_WebIdentity CreateClient() { var region = FallbackRegionFactory.GetRegionEndpoint() ?? _defaultSTSClientRegion; try { var stsConfig = ServiceClientHelpers.CreateServiceConfig(ServiceClientHelpers.STS_ASSEMBLY_NAME, ServiceClientHelpers.STS_SERVICE_CONFIG_NAME); stsConfig.RegionEndpoint = region; if (_options?.ProxySettings != null) { stsConfig.SetWebProxy(_options.ProxySettings); } return ServiceClientHelpers.CreateServiceFromAssembly( ServiceClientHelpers.STS_ASSEMBLY_NAME, ServiceClientHelpers.STS_SERVICE_CLASS_NAME, new AnonymousAWSCredentials(), region); } catch (Exception e) { var msg = string.Format(CultureInfo.CurrentCulture, "Assembly {0} could not be found or loaded. This assembly must be available at runtime to use Amazon.Runtime.AssumeRoleAWSCredentials.", ServiceClientHelpers.STS_ASSEMBLY_NAME); throw new InvalidOperationException(msg, e); } } } }