Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Facilitate injection of AWSCredentials #3716

Open
1 of 2 tasks
chase-miller opened this issue Mar 21, 2025 · 4 comments
Open
1 of 2 tasks

Facilitate injection of AWSCredentials #3716

chase-miller opened this issue Mar 21, 2025 · 4 comments
Labels
Extensions feature-request A feature should be added or improved. needs-review p2 This is a standard priority issue

Comments

@chase-miller
Copy link

Describe the feature

I'm hoping to inject into any class an instance of AWSCredentials. I'd like this AWSCredentials object to take into account Amazon.Extensions.NETCore.Setup.AWSOptions. The idea is that these credentials are resolved in the exact same way as the logic that creates ServiceClients.

Use Case

Aside from copying Amazon.Extensions.NETCore.Setup.CreateCredentials(), how would I do the following?

Class that makes a request against an endpoint that requires sigv4 headers:

public class SomeServiceClient
{
    private readonly IHttpClientFactory _httpClientFactory;

    public SomeServiceClient(IHttpClientFactory httpClientFactory, AWSCredentials credentials)
    {
        _httpClientFactory = httpClientFactory;
        _credentials = credentials;
    }

    public async Task PostStatus(INullContext bizContext, SomeRequestObject requestObject)
    {
        using var httpClient = _httpClientFactory.CreateClient();

        // Uses https://github.com/FantasticFiasco/aws-signature-version-4?tab=readme-ov-file#integration-with-httpclient to add sigv4 headers
        var response = await httpClient.PostAsync(
            "https://some-service.localhost/some-endpoint",
            JsonContent.Create(requestObject),
            "us-west-1",
            "execute-api",
            _credentials);

        response.EnsureSuccessStatusCode();
    }
}

Copy of Amazon.Extensions.NETCore.Setup.CreateCredentials():

public interface IAWSCredentialsFactory
{
    AWSCredentials Create();
}

public class DefaultAWSCredentialsFactory : IAWSCredentialsFactory
{
    private readonly AWSOptions? _options;
    private readonly ILogger<DefaultAWSCredentialsFactory>? _logger;

    public DefaultAWSCredentialsFactory(AWSOptions? options, ILogger<DefaultAWSCredentialsFactory>? logger = null)
    {
        _options = options;
        _logger = logger;
    }

    /// <summary>
    /// Creates the AWSCredentials using either AWSOptions.Credentials, AWSOptions.Profile + AWSOptions.ProfilesLocation,
    /// or the SDK fallback credentials search.
    /// </summary>
    public AWSCredentials Create()
    {
        if (_options != null)
        {
            if (_options.Credentials != null)
            {
                _logger?.LogInformation("Using AWS credentials specified with the AWSOptions.Credentials property");
                return _options.Credentials;
            }
            if (!string.IsNullOrEmpty(_options.Profile))
            {
                var chain = new CredentialProfileStoreChain(_options.ProfilesLocation);
                AWSCredentials result;
                if (chain.TryGetAWSCredentials(_options.Profile, out result))
                {
                    _logger?.LogInformation($"Found AWS credentials for the profile {_options.Profile}");
                    return result;
                }
                else
                {
                    _logger?.LogInformation($"Failed to find AWS credentials for the profile {_options.Profile}");
                }
            }
        }

        var credentials = FallbackCredentialsFactory.GetCredentials();
        if (credentials == null)
        {
            _logger?.LogError("Last effort to find AWS Credentials with AWS SDK's default credential search failed");
            throw new AmazonClientException("Failed to find AWS Credentials for constructing AWS service client");
        }
        else
        {
            _logger?.LogInformation("Found credentials using the AWS SDK's default credential search");
        }

        return credentials;
    }
}

Program.cs:

var builder = WebApplication.CreateBuilder(args);

builder.Services
    .AddDefaultAWSOptions(sp => sp.GetRequiredService<IConfiguration>().GetAWSOptions())
    .AddScoped<IAWSCredentialsFactory, DefaultAWSCredentialsFactory>()
    .AddScoped<AWSCredentials>(sp => sp.GetRequiredService<IAWSCredentialsFactory>().Create());

var app = builder.Build();

// add middleware

app.Run();

Test that verifies outbound call to endpoint requiring sigv4 headers:

[TestMethod]
public async Task TestMyEndpoint()
{
    // Setup
    var httpClientFactoryMock = Substitute.For<IHttpClientFactory>();
    var mockHttpHandler = new MockHttpMessageHandler();
    mockHttpHandler
        .When(HttpMethod.Post, "https://some-service.localhost/some-endpoint")
        .With(request => request.Headers.Any(header =>
            header.Key == "Authorization" &&
            header.Value.First().StartsWith("AWS4-HMAC-SHA256")))
        .Respond(HttpStatusCode.OK);
    httpClientFactoryMock.CreateClient().Returns(mockHttpHandler.ToHttpClient());

    var webAppFactory = new WebApplicationFactory<Program>()
        .WithWebHostBuilder(builder =>
        {
            builder.ConfigureServices(services =>
            {
                services.AddSingleton<IHttpClientFactory>(httpClientFactoryMock);

                services.AddDefaultAWSOptions(new AWSOptions
                {
                    Credentials = new BasicAWSCredentials("test", "test"),
                    Region = RegionEndpoint.GetBySystemName("us-west-1"),
                });
            });
        });

    using var httpClient = webAppFactory.CreateClient();

    // Execute
    var response = await httpClient.GetAsync("api/some-endpoint");

    // Verify
    response.StatusCode.Should().Be(HttpStatusCode.OK);
}

Proposed Solution

  • Extend AWSSDK.Extensions.NETCore.Setup's ServiceCollectionExtensions class to expose new AddCredentialsFactory() methods that register a new IAWSCredentialsFactory interface.
  • Move ClientFactory..CreateCredentials() to a new DefaultAWSCredentialsFactory class.
  • IAWSCredentialsFactory can then be injected into any consumer's class (and by extension obtain an AWSCredentials object).

This has the added benefit that consumers can customize the behavior of credential creation to meet their needs by registering their own implementation of IAWSCredentialsFactory.

Other Information

I've implemented these changes via PR #3715.

I believe that this would address #3461 and #3228.

Acknowledgements

  • I may be able to implement this feature request
  • This feature might incur a breaking change

AWS .NET SDK and/or Package version used

AWSSDK.Extensions.NETCore.Setup 3.7.400

Targeted .NET Platform

.NET 8

Operating System and version

macOS

@chase-miller chase-miller added feature-request A feature should be added or improved. needs-triage This issue or PR still needs to be triaged. labels Mar 21, 2025
@ashishdhingra ashishdhingra added Extensions p2 This is a standard priority issue labels Mar 21, 2025
@ashishdhingra
Copy link
Contributor

We might want to revisit this since FallbackCredentialsFactory is obsolete in V4 (currently in preview).

@ashishdhingra ashishdhingra added needs-review and removed needs-triage This issue or PR still needs to be triaged. labels Mar 21, 2025
@chase-miller
Copy link
Author

@ashishdhingra thanks for your reply!

We might want to revisit this since FallbackCredentialsFactory is obsolete in V4 (currently in preview).

Can you help me understand how we could leverage FallbackCredentialsFactory's successor to solve for this problem? I think there's still an opportunity for improvement for three reasons:

  1. DefaultIdentityResolverConfiguration doesn't appear to read AWSOptions.
  2. I still don't see a DI registration of AWSOptions (or a factory that produces it).
  3. The code executed as a result of calling AddAWSService<T> to create an IAmazonService implementation still passes AWSCredentials obtained via a private ClientFactory.CreateCredentials() method. Said differently, I don't think FallbackCredentialsFactory, IIdentityResolverConfiguration, or DefaultIdentityResolverConfiguration solve for this problem.

@chase-miller
Copy link
Author

@ashishdhingra bump :-). Reminder that I've implemented this in #3715.

@chase-miller
Copy link
Author

I just encountered another scenario where I needed to obtain AWSCredentials: extending AmazonS3Client:

    public class MyS3Client : AmazonS3Client
    {
        public MyS3Client(AWSCredentials credentials, AmazonS3Config clientConfig)
            : base(credentials, clientConfig)
        {

        }
    }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Extensions feature-request A feature should be added or improved. needs-review p2 This is a standard priority issue
Projects
None yet
Development

No branches or pull requests

2 participants