AWS SDK for .NET

De Banane Atomic
Aller à la navigationAller à la recherche

Links

Config

This file contains the profiles.

∼/.aws/config
[default]
region = eu-central-1

[profile Profile1]
sso_start_url  = https://my-sso-portal.awsapps.com/start
sso_region     = us-west-1
sso_account_id = 111122223333
sso_role_name  = SampleRole
region         = eu-central-1
output         = yaml-stream
services       = local-dynamodb

[services local-dynamodb]
dynamodb = 
  endpoint_url = http://localhost:8000
Ps.svg
aws sso login --profile Profile1

Define the AWS_PROFILE in an env var while starting the project.

Properties\launchSettings.json
{
  "profiles": {
    "MyProfile1": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "AWS_PROFILE": "Profile1"
      },
      "applicationUrl": "https://localhost:5001;http://localhost:5000"
    }
}

Credentials

This file contains credentials linked to profiles.

∼/.aws/credentials
[default]
aws_access_key_id     = ...
aws_secret_access_key = ...
aws_session_token     = ...

[Profile1]
key = value

Example .NET applications

Cs.svg
var ssoCreds = this.LoadSsoCredentials("Profile1");
var token = new AmazonSecurityTokenServiceClient(ssoCreds);

var caller = await token.GetCallerIdentityAsync(new GetCallerIdentityRequest());
this.userId = caller?.UserId.Substring(caller.UserId.IndexOf(":") + 1) ?? string.Empty;

var userNames = await this.GetIamUserNamesAsync(ssoCreds);
var bucketNames = await this.GetS3BucketNames(ssoCreds);

private AWSCredentials LoadSsoCredentials(string profile)
{
    var chain = new CredentialProfileStoreChain();
    if (!chain.TryGetAWSCredentials(profile, out var credentials))
    {
        errors.Add($"Failed to find the {profile} profile");
    }

    // set ClientName and launch a browser window that prompts the SSO user to complete an SSO login
    // if the session doesn't already have a valid SSO token.
    if (credentials is SSOAWSCredentials ssoCredentials)
    {
        ssoCredentials.Options.ClientName = "Example-SSO-App";
        ssoCredentials.Options.SsoVerificationCallback = args =>
        {
            Process.Start(new ProcessStartInfo
            {
                FileName = args.VerificationUriComplete,
                UseShellExecute = true
            });
        };
    }

    return credentials;
}

private async Task<IReadOnlyCollection<string>> GetIamUserNamesAsync(AWSCredentials ssoCreds)
{
    var iamClient = new AmazonIdentityManagementServiceClient(ssoCreds);
    var listResponse = await iamClient.ListUsersAsync();
    return listResponse.Users.Select(x => x.UserName).ToList();
}

private async Task<IReadOnlyCollection<string>> GetS3BucketNames(AWSCredentials ssoCreds)
{
    var s3Client = new AmazonS3Client(ssoCreds);
    // Amazon.Runtime.AmazonClientException: 'No RegionEndpoint or ServiceURL configured
    // define a default profile in config with a region
    var listResponse = await s3Client.ListBucketsAsync();
    return listResponse.Buckets.Select(x => x.BucketName).ToList();
}

Install the following nuget packages: AWSSDK.Core AWSSDK.SecurityToken AWSSDK.SSO AWSSDK.SSOOIDC
For IAM users: AWSSDK.IdentityManagement
For S3 buckets: AWSSDK.S3

Load .NET configuration from Secrets Manager

AmazonSecretsManagerConfigurationProvider.cs
public class AmazonSecretsManagerConfigurationProvider : ConfigurationProvider
{
    private readonly string secretName;

    public AmazonSecretsManagerConfigurationProvider(string secretName)
    {
        this.secretName = secretName;
    }

    public override void Load()
    {
        var secret = GetSecret();
        Data = JsonSerializer.Deserialize<Dictionary<string, string>>(secret)!;
    }

    private string GetSecret()
    {
        var request = new GetSecretValueRequest
        {
            SecretId = this.secretName
        };

        using (var client = new AmazonSecretsManagerClient())
        {
            var response = client.GetSecretValueAsync(request).Result;
            return response.SecretString;
        }
    }
}
AmazonSecretsManagerConfigurationSource.cs
public class AmazonSecretsManagerConfigurationSource : IConfigurationSource
{
    private readonly string secretName;

    public AmazonSecretsManagerConfigurationSource(string secretName)
    {
        this.secretName = secretName;
    }

    public IConfigurationProvider Build(IConfigurationBuilder builder)
    {
        return new AmazonSecretsManagerConfigurationProvider(this.secretName);
    }
}
ConfigurationBuilderExtensions.cs
public static class ConfigurationBuilderExtensions
{
    public static void AddAmazonSecretsManager(
        this IConfigurationBuilder configurationBuilder, string secretName)
    {
        var configurationSource = new AmazonSecretsManagerConfigurationSource(secretName);
        configurationBuilder.Add(configurationSource);
    }
}
Program.cs
builder.Configuration.AddAmazonSecretsManager("Secret name");

var secretValue = builder.Configuration["Secret key"];

Authentication with Cognito JWT Token

Program.cs
builder.Services.AddCognitoIdentity();

builder.Services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
    options.Authority = builder.Configuration["AWSCognito:Authority"];
    options.Audience = builder.Configuration["AWSCognito:UserPoolClientId"];
    options.TokenValidationParameters.AudienceValidator = (audiences, securityToken, validationParameters) =>
    {
        // This is necessary because Cognito access tokens doesn't have "aud" claim.
        // Instead the audience is set in "client_id"
        var jwt = (JsonWebToken)securityToken;
        var audience = jwt.Claims.FirstOrDefault(x => x.Type == "client_id" || x.Type == "aud");
        if (audience is null)
            return false;
        
        return validationParameters.ValidAudience == audience.Value;
    };
});

Install the following nuget packages: Amazon.AspNetCore.Identity.Cognito

aws-aspnet-cognito-identity-provider

Cognito is not a fully OIDC-compliant provider

Validate Issuer Signing Key

Useful id an ID token is used instead of an Access token.

Program.cs
var signingKeys = await GetSigningKeysAsync();

builder.Services.AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidateIssuerSigningKey = true, // needed if an ID token is used
        IssuerSigningKeys = signingKeys
    };
});

async Task<IList<JsonWebKey>> GetSigningKeysAsync()
{
    var httpClient = new HttpClient();
    var response = await httpClient.GetAsync($"{builder.Configuration["AWSCognito:Authority"]}/.well-known/jwks.json");
    var keySet = await response.Content.ReadAsAsync<JsonWebKeySet>();
    return keySet.Keys;
}

Get user info from Cognito in an ASP.NET web API

Usually a web API is called with an Access Token which doesn't contain information regarding the user but instead authorizations for actions.

The AccessToken needs to have the scope aws.cognito.signin.user.admin or no scope to be allowed to call cognitoService.GetUserAsync
Program.cs
builder.Services.AddHttpContextAccessor()
                .AddAWSService<IAmazonCognitoIdentityProvider>();
ApplicationUserProvider.cs
private readonly IHttpContextAccessor httpContextAccessor;
private readonly IAmazonCognitoIdentityProvider cognitoService;

public ApplicationUserProvider(IHttpContextAccessor httpContextAccessor, IAmazonCognitoIdentityProvider cognitoService)
{
    this.httpContextAccessor = httpContextAccessor;
    this.cognitoService = cognitoService;
}

public async Task GetUserInfo()
{
    var accessToken = await httpContextAccessor.HttpContext!.GetTokenAsync(OpenIdConnectParameterNames.AccessToken); // "access_token"
    if (accessToken is not null)
    {
        try
        {
            var userResponse = await cognitoService.GetUserAsync(new GetUserRequest
            {
                AccessToken = accessToken
            });

            var userId = userResponse.UserAttributes.Find(x => x.Name == "sub")?.Value;
            var userName = userResponse.UserAttributes.Find(x => x.Name == "name")?.Value;
            var userEmail = userResponse.UserAttributes.Find(x => x.Name == "email")?.Value;
        }
        catch (NotAuthorizedException noe) { } // Access Token does not have required scopes
    }
}