Authorization is the process that determines what users can do after they are authenticated. In the .NET ecosystem, it's straightforward to implement the most common approaches, such as role-based access control (RBAC) or attribute-based access control (ABAC). To show how this works, we will modify the code from the article Securing .NET APIs with Amazon Cognito to use these methods. The starting code is available for download here.
Role-based access control
In RBAC, access permissions are assigned to roles, and users are assigned to them. Open the solution, go to the MyWebApi
project, and update the endpoint definition in the Program.cs
file as shown below:
app.MapGet("/weatherforecast", [Authorize(Roles = "Premium")] () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
We are authorizing the endpoint only for users in the Premium
role. Open the template.yaml
file and add the following resource:
UserPoolGroup:
Type: AWS::Cognito::UserPoolGroup
Properties:
GroupName: Premium
Precedence: 0
UserPoolId: !Ref UserPool
The resource AWS::Cognito::UserPoolGroup
will create a group in Amazon Cognito. A group is a collection of users within a pool that shares permissions. Run the following commands to update the resources:
sam build
sam deploy --guided
Add a user to the group by running the following command:
aws cognito-idp admin-add-user-to-group --user-pool-id <MY_USER_POOL_ID> --username <MY_USER_NAME> --group-name Premium
As a result, the access and ID tokens both include a cognito:groups
claim that contains the user's groups. Go to the MyWebApi
project and update the AddJwtBearer
method in the Program.cs
file as shown below:
.AddJwtBearer(options =>
{
var configuration = builder.Configuration;
options.MetadataAddress = $"https://cognito-idp.{configuration["AWS:Region"]}.amazonaws.com/{configuration["AWS:UserPoolId"]}/.well-known/openid-configuration";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
ValidateIssuer = true,
ValidateLifetime = true,
ValidateAudience = true,
ValidAudience = configuration["AWS:UserPoolClientId"],
RoleClaimType = "cognito:groups",
AudienceValidator = (audiences, securityToken, validationParameters) =>
{
var token = securityToken as JsonWebToken;
var clientId = token?.GetClaim("client_id").Value;
return validationParameters.ValidateAudience ? validationParameters.ValidAudience.Equals(clientId) : true;
}
};
});
The default role's claim type is http://schemas.microsoft.com/ws/2008/06/identity/claims/role
. Therefore, we need to change the RoleClaimType
property to cognito:groups
. Run the application to see the RBAC authorization in action.
Attribute-based access control
In ABAC, access control policies are defined based on the user's attributes. Go to the MyWebApi
project and update the AddAuthorization
method in the Program.cs
file as follows:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Premium", policy => policy.RequireClaim("ispremium", "true"));
});
We are defining a policy named Premium
that requires a claim named ispremium
with the value set to true
. Then, update the endpoint definition as shown below:
app.MapGet("/weatherforecast", [Authorize(Policy = "Premium")] () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
We are authorizing the endpoint only for users who meet the Premium
policy. Next, we need to add a new attribute to the schema in the user pool. Once the attribute is assigned to the user, it will only be available in the ID token. A pre-token generation lambda trigger is needed to include the claim in the access token. Run the following commands to create the lambda function:
dotnet new lambda.EmptyFunction -n MyLambda -o .
dotnet add src/MyLambda package Amazon.Lambda.CognitoEvents
dotnet sln add --in-root src/MyLambda
Open the Function.cs
file and replace its content with:
using Amazon.Lambda.CognitoEvents;
using Amazon.Lambda.Core;
[assembly: LambdaSerializer(typeof(Amazon.Lambda.Serialization.SystemTextJson.DefaultLambdaJsonSerializer))]
namespace MyLambda;
public class Function
{
public CognitoPreTokenGenerationV2Event FunctionHandler(CognitoPreTokenGenerationV2Event input, ILambdaContext context)
{
var isPremium = false;
if (input.Request.UserAttributes.ContainsKey("custom:ispremium"))
{
isPremium = input.Request.UserAttributes["custom:ispremium"].Equals("true", StringComparison.InvariantCultureIgnoreCase);
}
input.Response = new CognitoPreTokenGenerationV2Response()
{
ClaimsAndScopeOverrideDetails = new ClaimsAndScopeOverrideDetails()
{
AccessTokenGeneration = new AccessTokenGeneration()
{
ClaimsToAddOrOverride = new Dictionary<string, string>() { { "ispremium", isPremium.ToString().ToLower() } }
},
GroupOverrideDetails = input.Request.GroupConfiguration
},
};
return input;
}
}
The CognitoPreTokenGenerationV2Event
includes a Request
property that contains all the user's attributes and a Response
property with an array of claims to be added to the access token. One thing to notice is that we are copying the original group information to the request to keep the cognito:groups
in the access token. Open the template.yaml
file and add the following resource:
PreTokenGenerationFunction:
Type: AWS::Serverless::Function
Properties:
Handler: MyLambda::MyLambda.Function::FunctionHandler
CodeUri: ./src/MyLambda/
Runtime: dotnet8
Timeout: 30
MemorySize: 512
Architectures:
- x86_64
CognitoPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !Ref PreTokenGenerationFunction
Principal: cognito-idp.amazonaws.com
SourceArn: !GetAtt UserPool.Arn
The AWS::Serverless::Function
resource deploys the lambda function, and the AWS::Lambda::Permission
lets Amazon Cognito invoke it. Update the following resource in the same file as shown:
UserPool:
Type: AWS::Cognito::UserPool
Properties:
UsernameAttributes:
- email
UsernameConfiguration:
CaseSensitive: false
Policies:
PasswordPolicy:
MinimumLength: 8
RequireLowercase: false
RequireNumbers: false
RequireSymbols: false
RequireUppercase: false
TemporaryPasswordValidityDays: 7
MfaConfiguration: 'OFF'
AccountRecoverySetting:
RecoveryMechanisms:
- Name: verified_email
Priority: 1
AdminCreateUserConfig:
AllowAdminCreateUserOnly: false
AutoVerifiedAttributes:
- email
UserPoolName: "myuserpool"
UserAttributeUpdateSettings:
AttributesRequireVerificationBeforeUpdate:
- email
Schema:
- Name: email
AttributeDataType: String
Mutable: false
Required: true
- Name: ispremium
AttributeDataType: Boolean
Mutable: true
Required: false
EmailConfiguration:
EmailSendingAccount: COGNITO_DEFAULT
UserPoolAddOns:
AdvancedSecurityMode: AUDIT
LambdaConfig:
PreTokenGenerationConfig:
LambdaArn: !GetAtt PreTokenGenerationFunction.Arn
LambdaVersion: V2_0
Here, we are adding the attribute ispremium
to the user's schema. We need the pre-token generation lambda version 2 to modify the access token. The feature requires the advanced security features enabled (UserPoolAddOns
property). Finally, in the PreTokenGenerationConfig
property, we referenced our lambda function and the version.
Run the following command to update the resources:
sam build
sam deploy --guided
Add the attribute to the user with the following command:
aws cognito-idp admin-update-user-attributes --user-pool-id <MY_USER_POOL_ID> --username <MY_USER_NAME> --user-attributes Name="custom:ispremium",Value="true"
Run the application to see the ABAC authorization in action. All the code can be found here. Thanks, and happy coding.