Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/MongoDB.Driver.Authentication.AWS/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ adjacent-areas: [Driver/Authentication]

This is an **optional** project providing the `MONGODB-AWS` SASL mechanism. Kept separate from `MongoDB.Driver` so that consumers who don't need AWS IAM auth aren't forced to ship the AWS SDK / signing dependencies.

For full coverage — credential resolution (explicit MongoCredential vs the AWS SDK's `FallbackCredentialsFactory` chain; consult the AWS SDK for the authoritative source order), SigV4 signing of `sts`-service requests, region derivation from the STS host header (defaulting to `us-east-1`), and registration via the `ISaslMechanismRegistry` exposed by `MongoClientSettings.Extensions.SaslMechanisms` — see the AWS IAM section in `src/MongoDB.Driver/Authentication/AGENTS.md`.
For full coverage — credential resolution (explicit MongoCredential vs the AWS SDK's `DefaultAWSCredentialsIdentityResolver` chain; consult the AWS SDK for the authoritative source order), SigV4 signing of `sts`-service requests, region derivation from the STS host header (defaulting to `us-east-1`), and registration via the `ISaslMechanismRegistry` exposed by `MongoClientSettings.Extensions.SaslMechanisms` — see the AWS IAM section in `src/MongoDB.Driver/Authentication/AGENTS.md`.

Wiring entry point: consumers call `MongoClientSettings.Extensions.AddAWSAuthentication()` once to opt into this assembly. That call resolves to the `ExtensionManagerExtensions.AddAWSAuthentication` extension method on `IExtensionManager` (in `src/MongoDB.Driver.Authentication.AWS/ExtensionManagerExtensions.cs`), which registers both `MONGODB-AWS` with `SaslMechanisms` and the AWS KMS provider with `KmsProviders`. The two are registered together because they share the same `aws-sdk-net` dependency this assembly carries — having opted in to the AWS SDK at all, registering the KMS provider too is essentially free and avoids a second opt-in for CSFLE consumers; auth-only consumers can leave `KmsProviders` empty and the KMS-side registration is inert. Nothing in this project registers itself at assembly load; the call is explicit.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,7 +141,6 @@ public void OnReAuthenticationRequired()

public bool TryHandleAuthenticationException(MongoException exception, ISaslStep step, SaslConversation conversation, ConnectionDescription description, out ISaslStep nextStep)
{
_credentialsSource.ResetCache();
nextStep = null;
return false;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,67 +16,30 @@
using System.Threading;
using System.Threading.Tasks;
using Amazon.Runtime;
using Amazon.Runtime.Credentials;

namespace MongoDB.Driver.Authentication.AWS.CredentialsSources
{
internal sealed class AWSFallbackCredentialsSource : IAWSCredentialsSource
{
public static readonly AWSFallbackCredentialsSource Instance = new();

private readonly SemaphoreSlim _lock = new(1);

public void Dispose() => _lock?.Dispose();

public AWSCredentials GetCredentials(CancellationToken cancellationToken)
{
Amazon.Runtime.AWSCredentials credentialsSource;
_lock.Wait(cancellationToken);
try
{
// returns cached credentials source immediately. Only if cached source unavailable, makes quite heavy steps
credentialsSource = FallbackCredentialsFactory.GetCredentials();
}
finally
{
_lock.Release();
}

cancellationToken.ThrowIfCancellationRequested();
var credentialsSource = DefaultAWSCredentialsIdentityResolver.GetCredentials(null);
var immutableCredentials = credentialsSource.GetCredentials();
return CreateAWSCredentials(immutableCredentials);
}

public async Task<AWSCredentials> GetCredentialsAsync(CancellationToken cancellationToken)
{
Amazon.Runtime.AWSCredentials credentialsSource;
await _lock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
// returns cached credentials source immediately. Only if cached source unavailable, makes quite heavy steps
credentialsSource = FallbackCredentialsFactory.GetCredentials();
}
finally
{
_lock.Release();
}

cancellationToken.ThrowIfCancellationRequested();
var credentialsSource = await DefaultAWSCredentialsIdentityResolver.GetCredentialsAsync(null).ConfigureAwait(false);

@adelinowona adelinowona Jul 28, 2026

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.

Also this probably doesn't have to block this PR any further (can be investigated in a separate ticket) but copilot had a comment about how our cancellationToken isn't passed to the AWS SDK. Upon inspecting the SDK code, it will use its own default cancellationToken of 35 seconds if we use the DefaultAWSCredentialsIdentityResolver.GetCredentials/GetCredentialsAsync methods. There is also ResolveIdentity(IClientConfig, CancellationToken) / ResolveIdentityAsync(...) methods which are instance methods that do take a cancellationToken so if we really wanted to fix this and honor our cancellationToken, we could create our own DefaultAWSCredentialsIdentityResolver instance and call ResolveIdentity(IClientConfig, CancellationToken) / ResolveIdentityAsync(...) so we can pass our cancellationToken.

Also worth investigating if IAWSCredentialsSource needs to be disposable since its dispose currently does nothing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with this, I'll create a follow up ticket.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

var immutableCredentials = await credentialsSource.GetCredentialsAsync().ConfigureAwait(false);
return CreateAWSCredentials(immutableCredentials);
}

public void ResetCache()
{
_lock.Wait();

try
{
FallbackCredentialsFactory.Reset();
}
finally
{
_lock.Release();
}
}

private AWSCredentials CreateAWSCredentials(ImmutableCredentials immutableCredentials)
{
var token = immutableCredentials.Token;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,10 @@ public AWSInstanceCredentialsSource(AWSCredentials credentials)
_credentials = credentials;
}

public void Dispose()
{
}

public AWSCredentials GetCredentials(CancellationToken cancellationToken)
=> _credentials;

public Task<AWSCredentials> GetCredentialsAsync(CancellationToken cancellationToken)
=> Task.FromResult(_credentials);

public void ResetCache()
{
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,15 @@
* limitations under the License.
*/

using System;
using System.Threading;
using System.Threading.Tasks;

namespace MongoDB.Driver.Authentication.AWS.CredentialsSources
{
internal interface IAWSCredentialsSource : IDisposable
internal interface IAWSCredentialsSource
{
AWSCredentials GetCredentials(CancellationToken cancellationToken);

Task<AWSCredentials> GetCredentialsAsync(CancellationToken cancellationToken);

void ResetCache();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AWSSDK.SecurityToken" Version="3.7.100.14" />
<PackageReference Include="AWSSDK.SecurityToken" Version="4.0.100.2" />
</ItemGroup>

<ItemGroup>
Expand Down
2 changes: 1 addition & 1 deletion src/MongoDB.Driver/Authentication/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Mid-operation token expiry: `TryHandleAuthenticationException` can refresh and r

Mechanism: `MONGODB-AWS`. Lives in its own NuGet package to avoid forcing the AWS SDK on every consumer. Files: `AWSSaslMechanism.cs`, `CredentialsSources/IAWSCredentialsSource.cs` (the abstraction), `CredentialsSources/AWSInstanceCredentialsSource.cs`, `CredentialsSources/AWSFallbackCredentialsSource.cs`, `SaslSteps/AWSFirstSaslStep.cs`, `SaslSteps/AWSLastSaslStep.cs`, `AWSSignatureVersion4.cs`. The `MONGODB-AWS` mechanism is wired into the global `ISaslMechanismRegistry` via `ExtensionManagerExtensions.cs`.

Credential resolution: explicit credentials supplied via `MongoCredential.CreateCredential("$external", username, password)` with `mechanism=MONGODB-AWS` (or via the connection-string equivalent) are handled by `AWSInstanceCredentialsSource`. There is no `CreateAwsIam` factory on `MongoCredential`. Anything else is delegated to the AWS SDK's `FallbackCredentialsFactory` (via `AWSFallbackCredentialsSource`), which owns the chain — see the AWS SDK for the authoritative ordering of sources (env vars, AppConfig/profile, `AssumeRoleWithWebIdentity`, ECS task role, EC2 instance profile via IMDS, etc.). Don't go hunting for chain logic in this tree; the SDK owns it.
Credential resolution: explicit credentials supplied via `MongoCredential.CreateCredential("$external", username, password)` with `mechanism=MONGODB-AWS` (or via the connection-string equivalent) are handled by `AWSInstanceCredentialsSource`. There is no `CreateAwsIam` factory on `MongoCredential`. Anything else is delegated to the AWS SDK's `DefaultAWSCredentialsIdentityResolver` (via `AWSFallbackCredentialsSource`), which owns the chain — see the AWS SDK for the authoritative ordering of sources (env vars, AppConfig/profile, `AssumeRoleWithWebIdentity`, ECS task role, EC2 instance profile via IMDS, etc.). Don't go hunting for chain logic in this tree; the SDK owns it.

Wire format: SigV4-signed `sts` service request (hard-coded in `AWSSignatureVersion4.cs` per the MONGODB-AWS spec) embedded in the SASL payload. Region is **derived from the STS host header** — `AWSSignatureVersion4.GetRegion(host)` hard-codes `us-east-1` for the exact host `sts.amazonaws.com`, splits any other host on `.` and takes the segment after the first dot (i.e. `split[1]`, e.g. `sts.us-west-2.amazonaws.com` → `us-west-2`), and falls back to `us-east-1` when no such segment is present. It is **not** parsed from the connection string or fetched from EC2 metadata. **No** speculative auth — `AWSSaslMechanism.CreateSpeculativeAuthenticationStep()` returns `null`; only SCRAM, X.509, and OIDC (with a non-expired cached token) speculate.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@
*/

using System;
using Amazon.Runtime;
using Amazon.Runtime.CredentialManagement;
using FluentAssertions;
using MongoDB.Bson;
using MongoDB.TestHelpers.XunitExtensions;
Expand Down Expand Up @@ -64,105 +62,5 @@ public void Ecs_should_fill_AWS_CONTAINER_CREDENTIALS_RELATIVE_URI()
var awsContainerUri = Environment.GetEnvironmentVariable("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") ?? Environment.GetEnvironmentVariable("AWS_CONTAINER_CREDENTIALS_FULL_URI");
(awsContainerUri != null).Should().Be(isEcs);
}

@papafe papafe Jul 8, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I added the following as an answer to a Copilot comment, but I think it's worth to add it here as well.

So... I'm not sure we need this test. For a couple of reasons:

  1. We don't have those methods used here anymore. FallbackCredentialsFactory.CredentialsGenerators is obsolete and its closest candidate AWSConfigs.AWSCredentialsGenerators is write only, so we can't read the default handlers.
  2. This test was essentially verifying that we get the "expected" chain of handlers to retrieve AWS credentials. Should we worry that the official SDK actually uses our "expected" chain of handlers?
  3. We actually verify that the correct credentials are chosen in our CI matrix with our run-aws-auth-test-with-... tasks.

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.

It seemed we were testing third-party library internals so I am supporting dropping the test. Unless there is a very good reason for doing so. @sanych-sun thoughts?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@sanych-sun Do you agree with removing this test?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm OK with dropping the test as long as there is no our code involved.

[Fact]
public void AwsSdk_should_support_all_required_handlers()
{
var credentialsGeneratorsDelegatesEnumerator = FallbackCredentialsFactory.CredentialsGenerators.GetEnumerator();

// AppConfigAWSCredentials
AWSCredentials credentials = null;
if (Type.GetType("Amazon.Runtime.AppConfigAWSCredentials, AWSSDK.Core", throwOnError: false) != null) // app.config/web.config does not present on windows
{
var appConfigAWSCredentialsException = Record.Exception(() => RunTestCase());
// app.config/web.config case is based on ConfigurationManager. This is not configured for this test
appConfigAWSCredentialsException.Message.Should().Contain("The app.config/web.config files for the application did not contain credential information");
}

// AssumeRoleWithWebIdentityCredentials.FromEnvironmentVariables()
var exception = Record.Exception(() => RunTestCase());
if (Environment.GetEnvironmentVariable("AWS_WEB_IDENTITY_TOKEN_FILE") != null)
{
// aws-web-identity-credentials is configured
exception.Should().BeNull();
credentials.Should().BeOfType<AssumeRoleWithWebIdentityCredentials>();
}
else
{
// otherwise fail
exception.Message.Should().Contain("webIdentityTokenFile");
}

// GetAWSCredentials (Profile)
exception = Record.Exception(() => RunTestCase());
if (IsWithAwsProfileOnMachine())
{
// current machine contains configured aws profile, which may include:
// 1. BasicAWSCredentials (aws_access_key_id and aws_secret_access_key)
// 2. SessionAWSCredentials (aws_access_key_id, aws_secret_access_key, aws_session_token)
exception.Should().BeNull();
credentials.Should().Match(x => x is BasicAWSCredentials || x is SessionAWSCredentials);
}
else
{
// otherwise fail
exception.Message.Should().Contain("Credential").And.Subject.Should().Contain("profile");
}

// EnvironmentVariablesAWSCredentials
exception = Record.Exception(() => RunTestCase());
if (Environment.GetEnvironmentVariable("AWS_ACCESS_KEY_ID") != null && Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") != null)
{
// environment variables code path
exception.Should().BeNull();
credentials.Should().BeOfType<EnvironmentVariablesAWSCredentials>();
}
else
{
// otherwise fail
exception.Message.Should().Contain("The environment variables").And.Subject.Contains("were not set with AWS credentials");
}

// ECSEC2CredentialsWrapper
exception = Record.Exception(() => RunTestCase());
if (Environment.GetEnvironmentVariable("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI") != null || Environment.GetEnvironmentVariable("AWS_CONTAINER_CREDENTIALS_FULL_URI") != null)
{
exception.Should().BeNull();
credentials.Should().BeOfType<ECSTaskCredentials>();
}
else
{
exception.Should().BeNull();
credentials.GetType().Name.Should().Contain("DefaultInstanceProfileAWSCredentials"); // EC2 case
}

credentialsGeneratorsDelegatesEnumerator.MoveNext().Should().BeFalse(); // no more handlers

bool IsWithAwsProfileOnMachine()
{
var credentialProfileChain = new CredentialProfileStoreChain();
if (credentialProfileChain.TryGetProfile(Environment.GetEnvironmentVariable("AWS_PROFILE") ?? "default", out var profile))
{
try
{
_ = profile.GetAWSCredentials(credentialProfileChain);
return true;
}
catch
{
return false;
}
}

return false;
}

void RunTestCase()
{
credentials = null;
credentialsGeneratorsDelegatesEnumerator.MoveNext().Should().BeTrue();
credentials = credentialsGeneratorsDelegatesEnumerator.Current();
}
}
}
}