aws-advanced-jdbc-wrapper.git / summary / log / commit / refs
commit 201e09cc074a36d9eafd2da668dabcdd39d7ea5d
Author: sergiyvamz <75754709+sergiyvamz@users.noreply.github.com>
Commit: GitHub <noreply@github.com>
Date: Tue Jul 28 15:57:05 2026 +0000
fix: honor awsProfile region for login_session IAM auth (#2050)
.../UsingTheIamAuthenticationPlugin.md | 15 +-
wrapper/build.gradle.kts | 4 +
.../software/amazon/jdbc/PropertyDefinition.java | 10 ++
.../jdbc/authentication/AwsCredentialsManager.java | 33 ++++-
.../LoginCredentialsProviderFactory.java | 158 +++++++++++++++++++++
.../plugin/AwsSecretsManagerConnectionPlugin.java | 4 +-
.../customendpoint/CustomEndpointPlugin.java | 2 +-
.../jdbc/plugin/iam/IamAuthConnectionPlugin.java | 4 +-
.../aws_advanced_jdbc_wrapper_messages.properties | 4 +
9 files changed, 220 insertions(+), 14 deletions(-)
diff --git a/docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md b/docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md
index 33d81ca4..ee9e378d 100644
--- a/docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md
+++ b/docs/using-the-jdbc-driver/using-plugins/UsingTheIamAuthenticationPlugin.md
@@ -56,13 +56,14 @@ IAM database authentication use is limited to certain database engines. For more
GRANT rds_iam TO db_userx;`
4. Add the plugin code `iam` to the [`wrapperPlugins`](../UsingTheJdbcDriver.md#connection-plugin-manager-parameters) parameter value.
-| Parameter | Value | Required | Description | Example Value |
-|------------------------------|:-------:|:--------:|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
-| `iamDefaultPort` | String | No | This property will override the default port that is used to generate the IAM token. The default port is determined based on the underlying driver protocol. For now, there is support for `jdbc:postgresql:` and `jdbc:mysql:`. Target drivers with different protocols will require users to provide a default port. | `1234` |
-| `iamHost` | String | No | This property will override the default hostname that is used to generate the IAM token. The default hostname is derived from the connection string. This parameter is required when users are connecting with custom endpoints. | `database.cluster-hash.us-east-1.rds.amazonaws.com` |
-| `iamRegion` | String | No | This property will override the default region that is used to generate the IAM token. The default region is parsed from the connection string. | `us-east-2` |
-| `iamExpiration` | Integer | No | This property determines how long an IAM token is kept in the driver cache before a new one is generated. The default expiration time is set to be 14 minutes and 30 seconds. Note that IAM database authentication tokens have a lifetime of 15 minutes. | `600` |
-| `iamAccessTokenPropertyName` | String | No | This property allows you to override the property name used for passing the IAM access token. Some underlying drivers may require a specific property name for IAM authentication. Default value is `password`. | `password`, `accessToken` |
+| Parameter | Value | Required | Description | Example Value |
+|------------------------------|:-------:|:--------:|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------------------------------------------|
+| `iamDefaultPort` | String | No | This property will override the default port that is used to generate the IAM token. The default port is determined based on the underlying driver protocol. For now, there is support for `jdbc:postgresql:` and `jdbc:mysql:`. Target drivers with different protocols will require users to provide a default port. | `1234` |
+| `iamHost` | String | No | This property will override the default hostname that is used to generate the IAM token. The default hostname is derived from the connection string. This parameter is required when users are connecting with custom endpoints. | `database.cluster-hash.us-east-1.rds.amazonaws.com` |
+| `iamRegion` | String | No | This property will override the default region that is used to generate the IAM token. The default region is parsed from the connection string. | `us-east-2` |
+| `iamExpiration` | Integer | No | This property determines how long an IAM token is kept in the driver cache before a new one is generated. The default expiration time is set to be 14 minutes and 30 seconds. Note that IAM database authentication tokens have a lifetime of 15 minutes. | `600` |
+| `iamAccessTokenPropertyName` | String | No | This property allows you to override the property name used for passing the IAM access token. Some underlying drivers may require a specific property name for IAM authentication. Default value is `password`. | `password`, `accessToken` |
+| `allowAwsLoginSession` | Boolean | No | When `true`, allows resolving AWS credentials from an `awsProfile` that authenticates via a `login_session` entry (the AWS Sign-In / browser-login flow). The connection region (see `iamRegion`) is used for the login/signin exchange so the configured profile is honored instead of falling back to the `default` profile. Requires the optional `software.amazon.awssdk:signin` dependency on the classpath. Default value is `false`. | `true` |
## Sample code
[AwsIamAuthenticationPostgresqlExample.java](../../../examples/AWSDriverExample/src/main/java/software/amazon/AwsIamAuthenticationPostgresqlExample.java)<br>
diff --git a/wrapper/build.gradle.kts b/wrapper/build.gradle.kts
index 3ee25c28..c2ce0a0c 100644
--- a/wrapper/build.gradle.kts
+++ b/wrapper/build.gradle.kts
@@ -54,6 +54,9 @@ dependencies {
optionalImplementation("software.amazon.awssdk:sts:$awsSdkVersion")
optionalImplementation("software.amazon.awssdk:kms:$awsSdkVersion")
optionalImplementation("software.amazon.awssdk:secretsmanager:$awsSdkVersion")
+ // Required only when 'allowAwsLoginSession=true' is used together with a profile that
+ // authenticates via a 'login_session' entry. Accessed reflectively so it stays optional.
+ optionalImplementation("software.amazon.awssdk:signin:$awsSdkVersion")
optionalImplementation("com.fasterxml.jackson.core:jackson-databind:2.22.1")
optionalImplementation("com.zaxxer:HikariCP:4.0.3") // Version 4.+ is compatible with Java 8
optionalImplementation("com.mchange:c3p0:0.14.1")
@@ -104,6 +107,7 @@ dependencies {
testImplementation("software.amazon.awssdk:ec2:$awsSdkVersion")
testImplementation("software.amazon.awssdk:secretsmanager:$awsSdkVersion")
testImplementation("software.amazon.awssdk:sts:$awsSdkVersion")
+ testImplementation("software.amazon.awssdk:signin:$awsSdkVersion")
// Note: all org.testcontainers dependencies should have the same version
testImplementation("org.testcontainers:testcontainers:$testcontainersVersion")
testImplementation("org.testcontainers:mysql:$testcontainersVersion")
diff --git a/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java b/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java
index 336073da..667fc671 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/PropertyDefinition.java
@@ -110,6 +110,16 @@ public class PropertyDefinition {
new AwsWrapperProperty(
"awsProfile", null, "Name of the AWS Profile to use for IAM/SecretsManager auth.");
+ public static final AwsWrapperProperty ALLOW_AWS_LOGIN_SESSION =
+ new AwsWrapperProperty(
+ "allowAwsLoginSession",
+ "false",
+ "Allow resolving AWS credentials from a profile that authenticates via a "
+ + "'login_session' entry. Requires the optional 'software.amazon.awssdk:signin' "
+ + "dependency on the classpath. When enabled, the connection region is used for the "
+ + "login/signin exchange so that the configured AWS profile is honored instead of "
+ + "falling back to the 'default' profile.");
+
public static final AwsWrapperProperty LOGIN_TIMEOUT =
new AwsWrapperProperty(
"loginTimeout", null, "Login timeout in msec.");
diff --git a/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java b/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java
index b8e790ef..da97fc18 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/authentication/AwsCredentialsManager.java
@@ -20,6 +20,7 @@ import java.util.Properties;
import org.checkerframework.checker.nullness.qual.Nullable;
import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider;
+import software.amazon.awssdk.regions.Region;
import software.amazon.jdbc.HostSpec;
import software.amazon.jdbc.PropertyDefinition;
import software.amazon.jdbc.util.ResourceLock;
@@ -43,20 +44,48 @@ public class AwsCredentialsManager {
}
public static AwsCredentialsProvider getProvider(final HostSpec hostSpec, final Properties props) {
+ return getProvider(hostSpec, props, null);
+ }
+
+ /**
+ * Resolves an {@link AwsCredentialsProvider} for the given connection, optionally scoping the
+ * AWS Sign-In ({@code login_session}) credential flow to the provided region.
+ *
+ * @param hostSpec the host being connected to.
+ * @param props the connection properties.
+ * @param region the region already resolved for the connection. Used to scope the login/signin
+ * exchange when {@code allowAwsLoginSession=true} and the configured profile
+ * authenticates via a {@code login_session} entry. May be null when no region has
+ * been resolved yet.
+ * @return the credentials provider to use for the connection.
+ */
+ public static AwsCredentialsProvider getProvider(
+ final HostSpec hostSpec, final Properties props, final @Nullable Region region) {
try (ResourceLock ignored = lock.obtain()) {
AwsCredentialsProvider provider = handler == null
? null
: handler.getAwsCredentialsProvider(hostSpec, props);
if (provider == null) {
- provider = getDefaultProvider(PropertyDefinition.AWS_PROFILE.getString(props));
+ provider = getDefaultProvider(props, region);
}
return provider;
}
}
- private static AwsCredentialsProvider getDefaultProvider(final @Nullable String awsProfileName) {
+ private static AwsCredentialsProvider getDefaultProvider(
+ final Properties props, final @Nullable Region region) {
+ final String awsProfileName = PropertyDefinition.AWS_PROFILE.getString(props);
+
+ if (PropertyDefinition.ALLOW_AWS_LOGIN_SESSION.getBoolean(props)) {
+ final AwsCredentialsProvider loginProvider =
+ LoginCredentialsProviderFactory.create(awsProfileName, region);
+ if (loginProvider != null) {
+ return loginProvider;
+ }
+ }
+
DefaultCredentialsProvider.Builder builder = DefaultCredentialsProvider.builder();
if (!StringUtils.isNullOrEmpty(awsProfileName)) {
builder.profileName(awsProfileName);
diff --git a/wrapper/src/main/java/software/amazon/jdbc/authentication/LoginCredentialsProviderFactory.java b/wrapper/src/main/java/software/amazon/jdbc/authentication/LoginCredentialsProviderFactory.java
new file mode 100644
index 00000000..ef337c4c
--- /dev/null
+++ b/wrapper/src/main/java/software/amazon/jdbc/authentication/LoginCredentialsProviderFactory.java
@@ -0,0 +1,158 @@
+/*
+ * 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.
+ * You may obtain a copy of the License at
+ *
+ * http://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.
+ */
+
+package software.amazon.jdbc.authentication;
+
+import java.lang.reflect.Method;
+import java.util.Optional;
+import org.checkerframework.checker.nullness.qual.Nullable;
+import software.amazon.awssdk.auth.credentials.AwsCredentialsProvider;
+import software.amazon.awssdk.profiles.Profile;
+import software.amazon.awssdk.profiles.ProfileFile;
+import software.amazon.awssdk.regions.Region;
+import software.amazon.jdbc.util.Messages;
+import software.amazon.jdbc.util.StringUtils;
+
+/**
+ * Builds an {@link AwsCredentialsProvider} for AWS profiles that authenticate via a
+ * {@code login_session} entry (the AWS Sign-In / browser-login credential flow).
+ *
+ * <p>The AWS SDK resolves {@code login_session} credentials through a {@code LoginCredentialsProvider}
+ * whose region is carried by an internally constructed {@code SigninClient}. When that provider is
+ * built through the profile-credentials factory (i.e. through {@code DefaultCredentialsProvider}),
+ * the {@code SigninClient} region is resolved via the SDK's {@code DefaultAwsRegionProviderChain},
+ * which ignores the configured AWS profile and falls back to the {@code default} profile. This
+ * factory instead constructs the provider with a {@code SigninClient} scoped to the region that the
+ * driver already resolved for the connection, so the configured profile is honored.
+ *
+ * <p>The {@code software.amazon.awssdk:signin} module is optional and is therefore accessed purely
+ * via reflection. This class only reaches the reflective code path when the connected profile
+ * actually declares a {@code login_session} entry.
+ */
+public class LoginCredentialsProviderFactory {
+
+ static final String LOGIN_SESSION_PROFILE_PROPERTY = "login_session";
+
+ private static final String DEFAULT_PROFILE_NAME = "default";
+ private static final String SIGNIN_CLIENT_CLASS =
+ "software.amazon.awssdk.services.signin.SigninClient";
+ private static final String SIGNIN_CLIENT_BUILDER_CLASS =
+ "software.amazon.awssdk.services.signin.SigninClientBuilder";
+ private static final String LOGIN_CREDENTIALS_PROVIDER_CLASS =
+ "software.amazon.awssdk.services.signin.auth.LoginCredentialsProvider";
+ private static final String LOGIN_CREDENTIALS_PROVIDER_BUILDER_CLASS =
+ "software.amazon.awssdk.services.signin.auth.LoginCredentialsProvider$Builder";
+
+ /**
+ * Attempts to build a login-session credentials provider for the given profile.
+ *
+ * @param awsProfileName the configured AWS profile name, or null/empty to use the default profile.
+ * @param region the region resolved for the connection, used to scope the signin exchange.
+ * @return a login-session credentials provider, or null if the profile does not use a
+ * {@code login_session} entry (in which case the caller should fall back to the standard
+ * credentials provider).
+ * @throws RuntimeException if the profile uses a {@code login_session} entry but the login flow
+ * cannot be set up (signin module missing, region unknown, or reflective construction failed).
+ */
+ public static @Nullable AwsCredentialsProvider create(
+ final @Nullable String awsProfileName, final @Nullable Region region) {
+
+ final String loginSession = getLoginSession(awsProfileName);
+ if (StringUtils.isNullOrEmpty(loginSession)) {
+ // Not a login_session profile; caller should use the standard credentials provider.
+ return null;
+ }
+
+ if (!isSigninModuleAvailable()) {
+ throw new RuntimeException(
+ Messages.get("AwsCredentialsManager.signinModuleNotInClasspath"));
+ }
+
+ if (region == null) {
+ throw new RuntimeException(
+ Messages.get("AwsCredentialsManager.loginSessionRegionRequired"));
+ }
+
+ return buildLoginCredentialsProvider(loginSession, region);
+ }
+
+ /**
+ * Reads the {@code login_session} property of the given profile from the default profile files.
+ *
+ * @param awsProfileName the profile name, or null/empty to use the default profile.
+ * @return the {@code login_session} value, or null if the profile is absent or has no such entry.
+ */
+ static @Nullable String getLoginSession(final @Nullable String awsProfileName) {
+ final String profileName =
+ StringUtils.isNullOrEmpty(awsProfileName) ? DEFAULT_PROFILE_NAME : awsProfileName;
+ try {
+ final ProfileFile profileFile = ProfileFile.defaultProfileFile();
+ final Optional<Profile> profile = profileFile.profile(profileName);
+ if (!profile.isPresent()) {
+ return null;
+ }
+ return profile.get().properties().get(LOGIN_SESSION_PROFILE_PROPERTY);
+ } catch (final RuntimeException e) {
+ // A malformed or unreadable profile file must not be interpreted as a login_session profile;
+ // let the caller fall back to the standard credentials provider.
+ return null;
+ }
+ }
+
+ static boolean isSigninModuleAvailable() {
+ try {
+ Class.forName(LOGIN_CREDENTIALS_PROVIDER_CLASS);
+ Class.forName(SIGNIN_CLIENT_CLASS);
+ return true;
+ } catch (final ClassNotFoundException e) {
+ return false;
+ }
+ }
+
+ private static AwsCredentialsProvider buildLoginCredentialsProvider(
+ final String loginSession, final Region region) {
+ try {
+ // SigninClient signinClient = SigninClient.builder().region(region).build();
+ final Class<?> signinClientClass = Class.forName(SIGNIN_CLIENT_CLASS);
+ final Class<?> signinClientBuilderClass = Class.forName(SIGNIN_CLIENT_BUILDER_CLASS);
+ final Object signinClientBuilder = signinClientClass.getMethod("builder").invoke(null);
+ signinClientBuilderClass.getMethod("region", Region.class)
+ .invoke(signinClientBuilder, region);
+ final Object signinClient =
+ signinClientBuilderClass.getMethod("build").invoke(signinClientBuilder);
+
+ // LoginCredentialsProvider.builder()
+ // .signinClient(signinClient)
+ // .loginSession(loginSession)
+ // .build();
+ final Class<?> loginProviderClass = Class.forName(LOGIN_CREDENTIALS_PROVIDER_CLASS);
+ final Class<?> loginProviderBuilderClass =
+ Class.forName(LOGIN_CREDENTIALS_PROVIDER_BUILDER_CLASS);
+ final Object loginProviderBuilder = loginProviderClass.getMethod("builder").invoke(null);
+ loginProviderBuilderClass.getMethod("signinClient", signinClientClass)
+ .invoke(loginProviderBuilder, signinClient);
+ loginProviderBuilderClass.getMethod("loginSession", String.class)
+ .invoke(loginProviderBuilder, loginSession);
+ final Method buildMethod = loginProviderBuilderClass.getMethod("build");
+ final Object provider = buildMethod.invoke(loginProviderBuilder);
+
+ return (AwsCredentialsProvider) provider;
+ } catch (final ReflectiveOperationException e) {
+ throw new RuntimeException(
+ Messages.get("AwsCredentialsManager.loginSessionProviderFailed", new Object[] {e}), e);
+ }
+ }
+}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPlugin.java
index 846dca56..f99e345f 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPlugin.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/AwsSecretsManagerConnectionPlugin.java
@@ -134,7 +134,7 @@ public class AwsSecretsManagerConnectionPlugin extends AbstractConnectionPlugin
try {
final URI endpointURI = new URI(endpoint);
return SecretsManagerClient.builder()
- .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props))
+ .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props, region))
.endpointOverride(endpointURI)
.region(region)
.build();
@@ -144,7 +144,7 @@ public class AwsSecretsManagerConnectionPlugin extends AbstractConnectionPlugin
}
} else {
return SecretsManagerClient.builder()
- .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props))
+ .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props, region))
.region(region)
.build();
}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/customendpoint/CustomEndpointPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/customendpoint/CustomEndpointPlugin.java
index 3a640974..d4de64e1 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/plugin/customendpoint/CustomEndpointPlugin.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/customendpoint/CustomEndpointPlugin.java
@@ -131,7 +131,7 @@ public class CustomEndpointPlugin extends AbstractConnectionPlugin implements St
(hostSpec, region) ->
RdsClient.builder()
.region(region)
- .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props))
+ .credentialsProvider(AwsCredentialsManager.getProvider(hostSpec, props, region))
.build());
}
diff --git a/wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java b/wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java
index 306791fc..3d0a62f9 100644
--- a/wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java
+++ b/wrapper/src/main/java/software/amazon/jdbc/plugin/iam/IamAuthConnectionPlugin.java
@@ -200,7 +200,7 @@ public class IamAuthConnectionPlugin extends AbstractConnectionPlugin implements
host.getHost(),
port,
region,
- AwsCredentialsManager.getProvider(hostSpec, props));
+ AwsCredentialsManager.getProvider(hostSpec, props, region));
LOGGER.finest(
() -> Messages.get(
"AuthenticationToken.generatedNewToken",
@@ -240,7 +240,7 @@ public class IamAuthConnectionPlugin extends AbstractConnectionPlugin implements
host.getHost(),
port,
region,
- AwsCredentialsManager.getProvider(hostSpec, props));
+ AwsCredentialsManager.getProvider(hostSpec, props, region));
LOGGER.finest(
() -> Messages.get(
"AuthenticationToken.generatedNewToken",
diff --git a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties
index d73230c9..cc0a9131 100644
--- a/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties
+++ b/wrapper/src/main/resources/aws_advanced_jdbc_wrapper_messages.properties
@@ -34,6 +34,10 @@ AuthenticationToken.generatedNewToken=Generated new authentication token = ''{0}
AuthenticationToken.javaSdkNotInClasspath=Required dependency 'AWS Java SDK RDS v2.x' is not on the classpath.
AuthenticationToken.rdsSdkNotInClasspath=Required dependency 'AWS Java SDK RDS v2.x' is not on the classpath. Only the full RDS SDK is supported when using Global databases.
+AwsCredentialsManager.signinModuleNotInClasspath=The configured AWS profile authenticates via a 'login_session' entry, but the required dependency 'software.amazon.awssdk:signin' is not on the classpath. Add it to use 'allowAwsLoginSession=true'.
+AwsCredentialsManager.loginSessionRegionRequired=Unable to determine the region required to resolve credentials for a 'login_session' AWS profile. Please set the connection region (for example, via the 'iamRegion' property for IAM authentication).
+AwsCredentialsManager.loginSessionProviderFailed=Failed to build the AWS Sign-In (login_session) credentials provider: ''{0}''
+
RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRDSProxy=An RDS Proxy url can''t be used as the 'clusterInstanceHostPattern' configuration setting.
RdsHostListProvider.clusterInstanceHostPatternNotSupportedForRdsCustom=A custom RDS url can''t be used as the 'clusterInstanceHostPattern' configuration setting.
RdsHostListProvider.invalidPattern=Invalid value for the 'clusterInstanceHostPattern' configuration setting - the host pattern must contain a '?' character as a placeholder for the DB instance identifiers of the instances in the cluster.
[parent: 4b5b78bfd7c0]