Skip to content

Add modular AWS credential resolution#1172

Open
mtdowling wants to merge 13 commits into
mainfrom
aws-auth
Open

Add modular AWS credential resolution#1172
mtdowling wants to merge 13 commits into
mainfrom
aws-auth

Conversation

@mtdowling
Copy link
Copy Markdown
Member

@mtdowling mtdowling commented May 7, 2026

Add modular credential chain with pluggable provider discovery

Adds a modular AWS credential provider chain that separates credential detection from resolution, enabling independent artifact composition.

New modules:

  • aws-config: SEP-conformant INI parser, AwsProfileFile, AwsProfile, AwsConfigCredentialSource sealed types. Standalone module with no chain dependency.
  • aws-credential-chain: CredentialChain, ChainIdentityProvider SPI, ChainSetup, StandardProvider enum with expanded profile-source slots, OrderingConstraint (Standard/Before/After), SharedConfigProvider, StaticKeysHandler, SessionKeysHandler, CredentialProcessHandler.
  • aws-credentials-imds: IMDSv2 credential provider with static stability, extended/legacy API fallback, config-based disable.

Changes to existing modules:

  • auth-api: Add CachingIdentityResolver with async background refresh, static stability, injectable Clock/ScheduledExecutorService. Add invalidate() default method to IdentityResolver.
  • aws-client-core: Add AwsCredentialChainPlugin, auth-failure interceptor, EnvironmentCredentialProvider and
    SystemPropertiesCredentialProvider (both terminal — stop chain assembly when credentials are present).
  • settings.gradle.kts: Include new modules.

Architecture:

  • One interface (ChainIdentityProvider), one SPI, void create() method.
  • Providers are sorted by ordering() then called in order with a shared mutable ChainSetup. They call setup.addResolver() to register, or setup.addTerminalResolver() to register and stop assembly.
  • StandardProvider enum encodes the full chain precedence including per-profile-source slots, matching the Credentials Provider Chain Search Precedence SEP exactly.
  • SharedConfigProvider parses config files and sets profile on setup; downstream profile providers read from setup.profile().
  • Terminal resolvers short-circuit assembly (env vars set → config never parsed, IMDS never created).
  • Actionable errors name the missing module when a source is detected but no provider claims its slot.
  • CachingIdentityResolver provides background refresh; invalidate() propagates through the chain to clear caches on auth failures.

TODO: STS, SSO, ECS, Login providers.


Example usage:

// build.gradle.kts
dependencies {
    implementation("software.amazon.smithy.java:aws-client-core:1.1.0")
    implementation("software.amazon.smithy.java:aws-credential-chain:1.1.0")
    implementation("software.amazon.smithy.java:aws-credentials-imds:1.1.0")
}
var client = CoffeeShopClient.builder()
        .addPlugin(new AwsCredentialChainPlugin())
        .build();

Credentials resolve automatically in order:

  1. JVM system properties (aws.accessKeyId) — terminal
  2. Environment variables (AWS_ACCESS_KEY_ID) — terminal
  3. Config file static/session keys — terminal
  4. Config file credential_process
  5. EC2 IMDS (if aws-credentials-imds is added)

On Lambda, only aws-client-core is needed (env vars are terminal, nothing else is constructed):

dependencies {
    implementation("software.amazon.smithy.java:aws-client-core:1.1.0")
    implementation("software.amazon.smithy.java:aws-credential-chain:1.1.0")
}

mtdowling added 8 commits May 11, 2026 15:59
Adds support for loading AWS credentials from shared config/credentials
files and assembling them into a pluggable credential provider chain.

New modules:
- aws-config: SEP-conformant INI parser, profile data model,
  AwsConfigCredentialSource sealed types, handler SPI with ServiceLoader
  discovery, and built-in handlers for static keys, session keys, and
  credential_process.
- aws-credential-chain: Credential provider chain with builtin slots,
  Before/After relative ordering, SPI-based provider discovery, cheap
  environment detection, and actionable error messages when
  implementation modules are missing.

Changes to existing modules:
- auth-api: Add CachingIdentityResolver with async background refresh,
  static stability support, injectable Clock and
  ScheduledExecutorService. Add invalidate() default method to
  IdentityResolver interface. Will be used by STS, SSO, etc.
- aws-client-core: Add AwsCredentialChainPlugin ClientPlugin, register
  EnvironmentCredentialProvider and SystemPropertiesCredentialProvider
  as chain sources via SPI. Both now read AWS_ACCOUNT_ID / aws.accountId
  per the account ID SEP.
- settings.gradle.kts: Include new modules.

Architecture overview:
- Data model (aws-config) is separated from resolution policy (chain).
- Credential sources are detected cheaply from profile properties
  without needing implementation modules (STS, SSO, IMDS).
- Handlers are discovered via ServiceLoader; missing handlers produce
  errors naming the dependency to add.
- Chain ordering uses a fixed enum for builtins and simple Before/After
  insertion for third-party providers.
- CachingIdentityResolver provides background refresh with a shared
  ScheduledExecutorService passed via ProviderContext.
- invalidate() propagates through the chain to force credential refresh
  on auth failures.

TODO items: add SSO, STS, IMDS, ECS, etc.
Allowing custom provider relative positioning around only builtins
simplifies the design, removes the possibility of cycles, and removes
the need for a topo sort. Also removing aliases.
Automatically add credential chain runtime plugin if AWS auth is
used.
@mtdowling mtdowling marked this pull request as draft May 12, 2026 14:59
@mtdowling mtdowling marked this pull request as ready for review May 12, 2026 19:54
mtdowling added 2 commits May 12, 2026 16:39
Reduces chain to a single, flat chain of fixed slots, including
config based credentials.
StringBuilder missing = new StringBuilder();
for (var i = 0; i < errors.size(); i += 2) {
if (i > 0) {
errors.add("; ");
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
errors.add("; ");
missing.append("; ");

Comment on lines +12 to +13
* Fuzz test for the AWS config file parser. Ensures no input can cause unexpected exceptions,
* OOM, or infinite loops. The only acceptable exception is {@link ConfigFileParseException}.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits

Suggested change
* Fuzz test for the AWS config file parser. Ensures no input can cause unexpected exceptions,
* OOM, or infinite loops. The only acceptable exception is {@link ConfigFileParseException}.
* Fuzz test for the AWS config file parser. Helps detect when an input causes unexpected exceptions,
* OOM, or infinite loops. The only acceptable exception is {@link ConfigFileParseException}.

String session = p.get("sso_session");
String account = p.get("sso_account_id");
String role = p.get("sso_role_name");
if (session == null || session.isEmpty()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, a isNullOrEmpty helper will help reading this code.

String homeDrive = envGetter.apply("HOMEDRIVE");
String homePath = envGetter.apply("HOMEPATH");
if (homeDrive != null && !homeDrive.isEmpty() && homePath != null && !homePath.isEmpty()) {
return Paths.get(homeDrive + homePath);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no idea how windows work but this feels better.

Suggested change
return Paths.get(homeDrive + homePath);
return Paths.get(homeDrive).resolve(homePath);

}

private static java.util.Set<String> profileNameSet(AwsProfileFile file) {
java.util.Set<String> names = new java.util.LinkedHashSet<>();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, replace FQN with import.

}

private static List<String> buildCommand(String commandLine) {
if (System.getProperty("os.name", "").toLowerCase(Locale.ROOT).contains("windows")) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit, consider adding a helper to detect windows.

Comment on lines +33 to +36
* <pre>{@code
* var chain = CredentialChain.create();
* var result = chain.resolveIdentity(Context.empty());
* }</pre>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <pre>{@code
* var chain = CredentialChain.create();
* var result = chain.resolveIdentity(Context.empty());
* }</pre>
* {@snippet lang="java" :
* var chain = CredentialChain.create();
* var result = chain.resolveIdentity(Context.empty());
* }


Services modeled with `@aws.auth#sigv4` or `@aws.auth#sigv4a` automatically get
`AwsCredentialChainPlugin` added as a default plugin during code generation.
No manual wiring needed - just add the provider modules you need to your
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yay!

static final ShapeId STS_SERVICE = ShapeId.from("com.amazonaws.sts#AWSSecurityTokenServiceV20110615");

private static final String STS_VERSION = "2011-06-15";
private static final String DEFAULT_ENDPOINT = "https://sts.amazonaws.com";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't we need to support regional endpoints declared in env or profile? See here

Comment on lines +23 to +27
* <pre>{@code
* MyClient.builder()
* .addPlugin(new AwsCredentialChainPlugin())
* .build();
* }</pre>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* <pre>{@code
* MyClient.builder()
* .addPlugin(new AwsCredentialChainPlugin())
* .build();
* }</pre>
* {@snippet lang="java" :
* MyClient.builder()
* .addPlugin(new AwsCredentialChainPlugin())
* .build();
* }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants