From b656ead9e0e07993109ef07f7644d22e3909586f Mon Sep 17 00:00:00 2001 From: Aleksandar Maksimovic Date: Thu, 12 Mar 2026 14:06:29 -0700 Subject: [PATCH] Add configurable token expiry to DSQL Other AWS SDKs (Go, Java, Ruby) allow configuring the auth token expiry duration up to 7 days. Add the same capability to the .NET SDK with a default of 15 minutes for backwards compatibility. --- .../Custom/Util/DSQLAuthTokenGenerator.cs | 84 +++++++++++++++++-- .../Custom/DSQLAuthTokenGeneratorTest.cs | 79 ++++++++++++++++- 2 files changed, 153 insertions(+), 10 deletions(-) diff --git a/sdk/src/Services/DSQL/Custom/Util/DSQLAuthTokenGenerator.cs b/sdk/src/Services/DSQL/Custom/Util/DSQLAuthTokenGenerator.cs index ab641474d450..c69e5c8f205a 100644 --- a/sdk/src/Services/DSQL/Custom/Util/DSQLAuthTokenGenerator.cs +++ b/sdk/src/Services/DSQL/Custom/Util/DSQLAuthTokenGenerator.cs @@ -39,6 +39,7 @@ public static class DSQLAuthTokenGenerator private const string XAmzExpires = "X-Amz-Expires"; private const string XAmzSecurityToken = "X-Amz-Security-Token"; private static readonly TimeSpan FifteenMinutes = TimeSpan.FromMinutes(15); + private static readonly TimeSpan MaxExpiresIn = TimeSpan.FromDays(7); /// /// AWS4PreSignedUrlSigner is built around operation request objects. @@ -116,7 +117,24 @@ public static string GenerateDbConnectAuthToken(AWSCredentials credentials, Regi throw new ArgumentNullException("credentials"); var immutableCredentials = credentials.GetCredentials(); - return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue, FifteenMinutes); + } + + /// + /// Generate a token for IAM authentication to a DSQL database cluster for the DbConnect action. + /// + /// The credentials for the token. + /// The region of the DSQL database. + /// Hostname of the DSQL database. + /// The token expiry duration. If not specified on other overloads, defaults to 15 minutes. + /// + public static string GenerateDbConnectAuthToken(AWSCredentials credentials, RegionEndpoint region, string hostname, TimeSpan expiresIn) + { + if (credentials == null) + throw new ArgumentNullException("credentials"); + + var immutableCredentials = credentials.GetCredentials(); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue, expiresIn); } /// @@ -182,7 +200,24 @@ public static async System.Threading.Tasks.Task GenerateDbConnectAuthTok throw new ArgumentNullException("credentials"); var immutableCredentials = await credentials.GetCredentialsAsync().ConfigureAwait(false); - return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue, FifteenMinutes); + } + + /// + /// Generate a token for IAM authentication to a DSQL database cluster for the DbConnect action. + /// + /// The credentials for the token. + /// The region of the DSQL database. + /// Hostname of the DSQL database. + /// The token expiry duration. If not specified on other overloads, defaults to 15 minutes. + /// + public static async System.Threading.Tasks.Task GenerateDbConnectAuthTokenAsync(AWSCredentials credentials, RegionEndpoint region, string hostname, TimeSpan expiresIn) + { + if (credentials == null) + throw new ArgumentNullException("credentials"); + + var immutableCredentials = await credentials.GetCredentialsAsync().ConfigureAwait(false); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectActionValue, expiresIn); } /// @@ -248,7 +283,24 @@ public static string GenerateDbConnectAdminAuthToken(AWSCredentials credentials, throw new ArgumentNullException("credentials"); var immutableCredentials = credentials.GetCredentials(); - return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue, FifteenMinutes); + } + + /// + /// Generate a token for IAM authentication to a DSQL database cluster for the DbConnectAdmin action. + /// + /// The credentials for the token. + /// The region of the DSQL database. + /// Hostname of the DSQL database. + /// The token expiry duration. If not specified on other overloads, defaults to 15 minutes. + /// + public static string GenerateDbConnectAdminAuthToken(AWSCredentials credentials, RegionEndpoint region, string hostname, TimeSpan expiresIn) + { + if (credentials == null) + throw new ArgumentNullException("credentials"); + + var immutableCredentials = credentials.GetCredentials(); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue, expiresIn); } /// @@ -314,10 +366,27 @@ public static async System.Threading.Tasks.Task GenerateDbConnectAdminAu throw new ArgumentNullException("credentials"); var immutableCredentials = await credentials.GetCredentialsAsync().ConfigureAwait(false); - return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue, FifteenMinutes); + } + + /// + /// Generate a token for IAM authentication to a DSQL database cluster for the DbConnectAdmin action. + /// + /// The credentials for the token. + /// The region of the DSQL database. + /// Hostname of the DSQL database. + /// The token expiry duration. If not specified on other overloads, defaults to 15 minutes. + /// + public static async System.Threading.Tasks.Task GenerateDbConnectAdminAuthTokenAsync(AWSCredentials credentials, RegionEndpoint region, string hostname, TimeSpan expiresIn) + { + if (credentials == null) + throw new ArgumentNullException("credentials"); + + var immutableCredentials = await credentials.GetCredentialsAsync().ConfigureAwait(false); + return GenerateAuthToken(immutableCredentials, region, hostname, DBConnectAdminActionValue, expiresIn); } - private static string GenerateAuthToken(ImmutableCredentials immutableCredentials, RegionEndpoint region, string hostname, string actionValue) + private static string GenerateAuthToken(ImmutableCredentials immutableCredentials, RegionEndpoint region, string hostname, string actionValue, TimeSpan expiresIn) { if (immutableCredentials == null) throw new ArgumentNullException("immutableCredentials"); @@ -329,12 +398,15 @@ private static string GenerateAuthToken(ImmutableCredentials immutableCredential if (string.IsNullOrEmpty(hostname)) throw new ArgumentException("Hostname must not be null or empty."); + if (expiresIn <= TimeSpan.Zero || expiresIn > MaxExpiresIn) + throw new ArgumentOutOfRangeException("expiresIn", "ExpiresIn must be between 0 (exclusive) and 7 days (inclusive)."); + GenerateDSQLAuthTokenRequest authTokenRequest = new GenerateDSQLAuthTokenRequest(); IRequest request = new DefaultRequest(authTokenRequest, DSQLServiceName); request.UseQueryString = true; request.HttpMethod = HTTPGet; - request.Parameters.Add(XAmzExpires, FifteenMinutes.TotalSeconds.ToString(CultureInfo.InvariantCulture)); + request.Parameters.Add(XAmzExpires, expiresIn.TotalSeconds.ToString(CultureInfo.InvariantCulture)); request.Parameters.Add(ActionKey, actionValue); request.Endpoint = new UriBuilder(HTTPS, hostname).Uri; diff --git a/sdk/test/Services/DSQL/UnitTests/Custom/DSQLAuthTokenGeneratorTest.cs b/sdk/test/Services/DSQL/UnitTests/Custom/DSQLAuthTokenGeneratorTest.cs index 622b9285a125..6b3fcf2e7a0e 100644 --- a/sdk/test/Services/DSQL/UnitTests/Custom/DSQLAuthTokenGeneratorTest.cs +++ b/sdk/test/Services/DSQL/UnitTests/Custom/DSQLAuthTokenGeneratorTest.cs @@ -175,6 +175,54 @@ public void GenerateDbConnectAuthTokenEmptyHostname() }, typeof(ArgumentException)); } + [TestMethod] + [TestCategory("DSQL")] + public void GenerateDbConnectAuthTokenCustomExpiresIn() + { + AssertAuthToken(DSQLAuthTokenGenerator.GenerateDbConnectAuthToken(BasicCredentials, + AWSRegion, DBCluster, TimeSpan.FromSeconds(450)), AccessKey, AWSRegion, DBConnectActionValue, false, 450); + } + + [TestMethod] + [TestCategory("DSQL")] + public void GenerateDbConnectAuthTokenZeroExpiresIn() + { + AssertExtensions.ExpectException(() => + { + DSQLAuthTokenGenerator.GenerateDbConnectAuthToken(BasicCredentials, AWSRegion, DBCluster, TimeSpan.Zero); + }, typeof(ArgumentOutOfRangeException)); + } + + [TestMethod] + [TestCategory("DSQL")] + public void GenerateDbConnectAuthTokenNegativeExpiresIn() + { + AssertExtensions.ExpectException(() => + { + DSQLAuthTokenGenerator.GenerateDbConnectAuthToken(BasicCredentials, AWSRegion, DBCluster, TimeSpan.FromSeconds(-1)); + }, typeof(ArgumentOutOfRangeException)); + } + + [TestMethod] + [TestCategory("DSQL")] + public void GenerateDbConnectAuthTokenExpiresInExceeds7Days() + { + AssertExtensions.ExpectException(() => + { + DSQLAuthTokenGenerator.GenerateDbConnectAuthToken(BasicCredentials, AWSRegion, DBCluster, TimeSpan.FromDays(8)); + }, typeof(ArgumentOutOfRangeException)); + } + +#if ASYNC_AWAIT + [TestMethod] + [TestCategory("DSQL")] + public async System.Threading.Tasks.Task GenerateDbConnectAuthTokenCustomExpiresInAsync() + { + AssertAuthToken(await DSQLAuthTokenGenerator.GenerateDbConnectAuthTokenAsync(BasicCredentials, + AWSRegion, DBCluster, TimeSpan.FromSeconds(450)), AccessKey, AWSRegion, DBConnectActionValue, false, 450); + } +#endif + // DbConnectAdmin #if ASYNC_AWAIT @@ -275,12 +323,35 @@ public void GenerateDbConnectAdminAuthTokenEmptyHostname() }, typeof(ArgumentException)); } + [TestMethod] + [TestCategory("DSQL")] + public void GenerateDbConnectAdminAuthTokenCustomExpiresIn() + { + AssertAuthToken(DSQLAuthTokenGenerator.GenerateDbConnectAdminAuthToken(BasicCredentials, + AWSRegion, DBCluster, TimeSpan.FromSeconds(450)), AccessKey, AWSRegion, DBConnectAdminActionValue, false, 450); + } + +#if ASYNC_AWAIT + [TestMethod] + [TestCategory("DSQL")] + public async System.Threading.Tasks.Task GenerateDbConnectAdminAuthTokenCustomExpiresInAsync() + { + AssertAuthToken(await DSQLAuthTokenGenerator.GenerateDbConnectAdminAuthTokenAsync(BasicCredentials, + AWSRegion, DBCluster, TimeSpan.FromSeconds(450)), AccessKey, AWSRegion, DBConnectAdminActionValue, false, 450); + } +#endif + private void AssertAuthToken(string token, string accessKey, RegionEndpoint region, string actionValue) { - AssertAuthToken(token, accessKey, region, actionValue, false); + AssertAuthToken(token, accessKey, region, actionValue, false, 900); } private void AssertAuthToken(string token, string accessKey, RegionEndpoint region, string actionValue, bool hasSessionToken) + { + AssertAuthToken(token, accessKey, region, actionValue, hasSessionToken, 900); + } + + private void AssertAuthToken(string token, string accessKey, RegionEndpoint region, string actionValue, bool hasSessionToken, int expectedExpiresInSeconds) { // Look for today or yesterday to cover the crazy case where the // token was generated utc yesterday but we're asserting utc today. @@ -289,9 +360,9 @@ private void AssertAuthToken(string token, string accessKey, RegionEndpoint regi var sessionTokenPart = hasSessionToken ? "X-Amz-Security-Token=" + AWSSDKUtils.UrlEncode(SessionToken, false) + "&" : ""; var regex = Regex.Escape(string.Format(CultureInfo.InvariantCulture, - "{0}/?Action={1}&X-Amz-Expires=900&{2}X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=" + - "{3}%2FTODAYREGEX%2F{4}%2Fdsql%2Faws4_request&X-Amz-Date=TODAYREGEXTTIMEREGEXZ&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATUREREGEX", - DBCluster, actionValue, sessionTokenPart, accessKey, region.SystemName)); + "{0}/?Action={1}&X-Amz-Expires={2}&{3}X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=" + + "{4}%2FTODAYREGEX%2F{5}%2Fdsql%2Faws4_request&X-Amz-Date=TODAYREGEXTTIMEREGEXZ&X-Amz-SignedHeaders=host&X-Amz-Signature=SIGNATUREREGEX", + DBCluster, actionValue, expectedExpiresInSeconds, sessionTokenPart, accessKey, region.SystemName)); regex = regex.Replace("TIMEREGEX", "[0-9]{6}").Replace("SIGNATUREREGEX", "[0-9a-f]{64}").Replace("TODAYREGEX", todayRegex); Assert.IsTrue(Regex.IsMatch(token, regex), token + " doesn't match regex " + regex);