aws / aws/aws-sdk-net

Facilitate injection of AWSCredentials

Open
#3,716 4 comments 0 reactions 0 assignees View on GitHub
Extensions feature-request p2 queued
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.