Facilitate injection of AWSCredentials
- Dominant language
- C#
- Stars
- 140
- Forks
- 891
- Avg merge
- 21h 51m
- Merged PRs (30d)
- 10
Description
### 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:
```cs
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()`:
```cs
public interface IAWSCredentialsFactory
{
AWSCredentials Create();
}
public class DefaultAWSCredentialsFactory : IAWSCredentialsFactory
{
private readonly AWSOptions? _options;
private readonly ILogger? _logger;
public DefaultAWSCredentialsFactory(AWSOptions? options, ILogger? logger = null)
{
_options = options;
_logger = logger;
}
///
/// Creates the AWSCredentials using either AWSOptions.Credentials, AWSOptions.Profile + AWSOptions.ProfilesLocation,
/// or the SDK fallback credentials search.
///
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:
```cs
var builder = WebApplication.CreateBuilder(args);
builder.Services
.AddDefaultAWSOptions(sp => sp.GetRequiredService().GetAWSOptions())
.AddScoped()
.AddScoped(sp => sp.GetRequiredService().Create());
var app = builder.Build();
// add middleware
app.Run();
```
Test that verifies outbound call to endpoint requiring sigv4 headers:
```cs
[TestMethod]
public async Task TestMyEndpoint()
{
// Setup
var httpClientFactoryMock = Substitute.For();
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()
.WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton(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 https://github.com/aws/aws-sdk-net/pull/3715.
I believe that this would address https://github.com/aws/aws-sdk-net/issues/3461 and https://github.com/aws/aws-sdk-net/issues/3228.
### Acknowledgements
- [x] 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
Contributor guide
Research direction
Start with PR 3715 and the AWSSDK.Extensions.NETCore.Setup ServiceCollectionExtensions and ClientFactory.CreateCredentials references. Compare the proposed IAWSCredentialsFactory and DefaultAWSCredentialsFactory registration with existing service-client credential resolution and the supplied integration-test scenario. Done means credentials can be registered and injected while preserving AWSOptions behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp
- Domain
- authentication, backend
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Stale
- Clarity
- Clearly specified
- Newbie friendliness
- 25/100