« AWS SDK for .NET » : différence entre les versions

De Banane Atomic
Aller à la navigationAller à la recherche
Aucun résumé des modifications
 
(45 versions intermédiaires par le même utilisateur non affichées)
Ligne 1 : Ligne 1 :
[[Category:AWS]]
[[Category:AWS]]
= Secrets Manager =
= Links =
* [https://repost.aws/knowledge-center/decode-verify-cognito-json-token Decode and verify the signature of an Cognito JWT]
* [https://docs.aws.amazon.com/cognito/latest/developerguide/user-pool-lambda-pre-token-generation.html#user-pool-lambda-pre-token-generation-accesstoken Customizing the access token]
* [https://aws.amazon.com/blogs/compute/evaluating-access-control-methods-to-secure-amazon-api-gateway-apis/ Access control methods to secure Amazon API Gateway APIs]
* [[Lambda]]
 
= [https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-config Config] =
This file contains the profiles.
<filebox fn='∼/.aws/config' lang='ini'>
[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
</filebox>
 
<kode lang='ps'>
aws sso login --profile Profile1
</kode>
 
Define the {{boxx|AWS_PROFILE}} in an env var while starting the project.
<filebox fn='Properties\launchSettings.json' collapsed>
{
  "profiles": {
    "MyProfile1": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "swagger",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development",
        "AWS_PROFILE": "Profile1"
      },
      "applicationUrl": "https://localhost:5001;http://localhost:5000"
    }
}
</filebox>
 
= [https://docs.aws.amazon.com/sdkref/latest/guide/file-format.html#file-format-creds Credentials] =
This file contains credentials linked to profiles.
<filebox fn='∼/.aws/credentials' lang='ini'>
[default]
aws_access_key_id    = ...
aws_secret_access_key = ...
aws_session_token    = ...
 
[Profile1]
key = value
</filebox>
 
= [https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/sso-tutorial-app-only.html#sso-tutorial-app-only-code Example .NET applications] =
<kode lang='cs' collapsed>
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();
}
</kode>
 
Install the following nuget packages: {{boxx|AWSSDK.Core}} {{boxx|AWSSDK.SecurityToken}} {{boxx|AWSSDK.SSO}} {{boxx|AWSSDK.SSOOIDC}}<br>
For IAM users: {{boxx|AWSSDK.IdentityManagement}}<br>
For S3 buckets: {{boxx|AWSSDK.S3}}
 
= [https://referbruv.com/blog/securing-aspnet-core-apis-with-jwt-bearer-using-aws-cognito/ Authentication with Cognito JWT Token] =
<filebox fn='Program.cs'>
<filebox fn='Program.cs'>
builder.Configuration.Sources.Add(new AmazonSecretsManagerConfigurationSource("secretName"));
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;
    };
});
</filebox>
 
Install the following nuget packages: {{boxx|Amazon.AspNetCore.Identity.Cognito}}
 
[https://github.com/aws/aws-aspnet-cognito-identity-provider aws-aspnet-cognito-identity-provider]
 
{{warn | Cognito is not a fully OIDC-compliant provider}}
 
== Validate Issuer Signing Key  ==
Useful id an ID token is used instead of an Access token.
<filebox fn='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;
}
</filebox>
* [https://medium.com/@marcio_30193/jwt-machine-to-machine-usando-aws-cognito-and-c-b1fab0524712 JWT “Machine to Machine” usando AWS Cognito and C#]
* [https://stackoverflow.com/questions/66892866/issuersigningkeyresolver-call-async-method IssuerSigningKeyResolver call async method]
 
= 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.
{{warn | The AccessToken needs to have the scope {{boxx|aws.cognito.signin.user.admin}} or no scope to be allowed to call {{boxx|cognitoService.GetUserAsync}}}}
 
<filebox fn='Program.cs' collapsed>
builder.Services.AddHttpContextAccessor()
                .AddAWSService<IAmazonCognitoIdentityProvider>();
</filebox>
 
<filebox fn='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
    }
}
</filebox>
</filebox>

Dernière version du 10 juin 2024 à 15:59

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

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
    }
}