Introduction
Are you having trouble managing user authentication and security in your .NET applications? You might have seen examples online that only cover part of what you need. I encountered similar challenges while working on a project that required securing a .NET application with AWS. After some research and trial and error, I secured the application and decided to write this comprehensive guide on managing users and groups with AWS Cognito & .NET.
Whether you are new to AWS Cognito or looking for a better way to manage your users and groups, this guide provides the technical knowledge and practical examples you need.
Let’s get started!
What is AWS Cognito?
Amazon Cognito is a fully managed user identity service designed to simplify authentication and user management for web and mobile applications. It provides a way to add sign-up, sign-in, and access control to applications. With Cognito, we can create user pools that act as secure directories for storing and managing user profiles. It also supports federated identities, allowing users to log in using external providers like Google, Facebook, Apple, or corporate directories via SAML or OIDC.
Cognito also takes care of routine tasks such as password resets, account recovery, and user profile updates.
Managing users, groups, and access control in a service like AWS Cognito & .NET (or similar services from other providers), leaves us more time to focus on the core functionality of the application instead of dealing with the underlying authentication plumbing.
What does this guide cover?
This guide covers the following topics:
- Setting up a .NET Core application to integrate with AWS Cognito.
- Creating, deleting, retrieving, and updating user accounts and groups.
- Implementing basic multi-tenancy with Cognito.
Prerequisites
- AWS Account – You can create one here – ***https://aws.amazon.com/free/***.
- AWS SDK for .NET – We will install the required packages along the way.
- .NET 8.0 – Available here Download .NET 8.0 (Linux, macOS, and Windows)
Setting Up AWS Cognito
Before diving into code, you need to have AWS Cognito set up. This post focuses on integrating Cognito with your .NET Core application rather than covering all the detailed and advanced setup topics. For a complete guide on setting up Cognito, please refer to the official AWS documentation.
Step 1: Setting up user pools in Cognito has become simpler. After logging in to your account in AWS, navigate to AWS Cognito then select the “Create User Pool” button (Should be on the top left corner)
Step 2: We will be communicating with Cognito via API calls from C#, so select “Traditional Web Application”. Select a name for the application under “Name your application”, check “Email” under “Configure Options”, then “Create user directory” button:

Cognito creates an App Client for you by default. This App Client acts as a bridge between your application and the Cognito user pool, handling tasks like user authentication and token management. It simplifies sign-up, sign-in, and token refresh processes, ensuring that your app interacts securely with Cognito.
If you need to change the default settings of your App Client, open it from the “App clients” section under the “Applications” menu. There, you can find the Client ID and Client Secret, which you will need soon. While you can adjust other settings as well, this post focuses on coding, so we’ll stick with the defaults.
Project Setup & Configuration
We’ll start by creating a new .Net Core project:
dotnet new webapi -n AWSCognitoUserMgmt
Once the project was created, we need to add the required nuget packages to work with AWS Cognito:
dotnet add AWSCognitoUserMgmt.csproj package AWSSDK.CognitoIdentityProvider
Now that the basic packages are in place, we can create and initialize a Cognito client. This client lets your application make programmatic calls to AWS Cognito by using the application pool ID and client ID, which you can find in the AWS Cognito console.
For simplicity, this example stores these values locally and read them during application startup. However, in production it’s important to store these keys securely and load them safely at startup.
We’ll create the simple class to hold the required configuration for the client:
public class IdentityProviderConfiguration
{
public string Region { get; set; } = string.Empty;
public string PoolId { get; set; } = string.Empty;
public string ClientId { get; set; } = string.Empty;
}
Next, we’ll read the configuration during startup:
builder.Services.Configure<IdentityProviderConfiguration>(
builder.Configuration.GetSection("AWS:Cognito"));
Next, we will create a new class that has the Cognito client to make the calls to AWS:
public class CognitoService : ICognitoService
{
private readonly IAmazonCognitoIdentityProvider _cognitoClient;
private readonly string _userPoolId;
private readonly string _clientId;
private readonly ILogger<CognitoService> _logger;
public CognitoService(IAmazonCognitoIdentityProvider cognitoClient,
IOptions<IdentityProviderConfiguration> options,
ILogger<CognitoService> logger)
{
_cognitoClient = cognitoClient ?? throw new ArgumentNullException(nameof(cognitoClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
var config = options?.Value ?? throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(config.PoolId))
throw new ArgumentException("User pool ID cannot be null or empty", nameof(config.PoolId));
if (string.IsNullOrWhiteSpace(config.ClientId))
throw new ArgumentException("Client ID cannot be null or empty", nameof(config.ClientId));
_userPoolId = config.PoolId;
_clientId = config.ClientId;
}
}
Next, we add the IAmazonCognitoIdentityProvider to our dependency injection container. We use the configuration class we created earlier to set the AWS region where our Cognito setup is located. Finally, we register our CognitoService class in the DI so it can be used throughout the application.
// Register AmazonCognitoIdentityProviderClient with DI
builder.Services.AddSingleton<IAmazonCognitoIdentityProvider>(sp =>
{
var config = sp.GetRequiredService<IOptions<IdentityProviderConfiguration>>().Value;
var awsRegion = RegionEndpoint.GetBySystemName(config.Region);
return new AmazonCognitoIdentityProviderClient(awsRegion);
});
builder.Services.AddScoped<ICognitoService, CognitoService>();
That we have the basic setup ready, we can move and start writing the code to work Cognito.
Working with Users in AWS Cognito
Creating a User
We can create user by calling AdminCreateUserAsync with the proper request:
public async Task<AdminCreateUserResponse> CreateUserAsync(string username, string password)
{
try
{
var request = new AdminCreateUserRequest
{
UserPoolId = _userPoolId,
Username = username,
TemporaryPassword = password,
UserAttributes = new List<AttributeType>
{
new AttributeType { Name = "email", Value = username }
}
};
var response = await _cognitoClient.AdminCreateUserAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} created successfully in Cognito.", username);
return response;
}
catch (UsernameExistsException ex)
{
_logger.LogWarning(ex, "User {Username} already exists in Cognito.", username);
throw;
}
catch (TooManyRequestsException ex)
{
_logger.LogError(ex, "Too many requests to AWS Cognito while creating user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while creating user {Username} in Cognito.", username);
throw;
}
}
In this code, we are creating a new user in AWS Cognito using the AdminCreateUserAsync method. First, we build a request object that includes the user pool ID, username, temporary password, and user attributes (in this case, the user’s email). We then call AWS Cognito to create the user and handle any potential errors, such as if the username already exists or if there are too many requests sent in a short period.
Deleting a User
public async Task<AdminDeleteUserResponse> DeleteUserAsync(string username)
{
try
{
var request = new AdminDeleteUserRequest
{
UserPoolId = _userPoolId,
Username = username
};
var response = await _cognitoClient.AdminDeleteUserAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} deleted successfully from Cognito.", username);
return response;
}
catch (UserNotFoundException ex)
{
_logger.LogWarning(ex, "Attempted to delete non-existent user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while deleting user {Username} from Cognito.", username);
throw;
}
}
Deleting a user is done by calling AdminDeleteUserAsync which takes AdminDeleteUserRequest` that has the UserPoolId and the UserName we want to delete. Notice that this method might throw UserNotFoundException in case not such user exist.
Retrieving and Listing Users
Retrieving users from Cognito can be done in more than one way, as we can get a single user directly using AdminGetUserAsync API or by making queries. let’s see how we can do both.
Getting Single User
public async Task<AdminGetUserResponse> GetUserAsync(string username)
{
try
{
var request = new AdminGetUserRequest
{
UserPoolId = _userPoolId,
Username = username
};
var response = await _cognitoClient.AdminGetUserAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} retrieved successfully from Cognito.", username);
return response;
}
catch (UserNotFoundException ex)
{
_logger.LogWarning(ex, "Attempted to retrieve non-existent user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while retrieving user {Username} from Cognito.", username);
throw;
}
}
Another way to list users in Cognito is by searching user attributes, which returns all matching results. However, as of this writing, searching custom attributes is not supported.
Cognito allows filtering users based on the following attributes:
username
(case-sensitive)email
phone_number
name
given_name
family_name
preferred_username
cognito:user_status
(Status in the Console) (case-insensitive)status
(Enabled in the Console) (case-sensitive)sub
To perform a search, we use a Filter
, which consists of an attribute name, an operator, and a value. We can also build more complex queries using AND
and OR
to combine multiple filter conditions.
Let’s see an example to do this.
// Search all users with certain family_name
public async Task<ListUsersResponse> ListUsersAsync(string familyName)
{
try
{
var request = new ListUsersRequest
{
UserPoolId = _userPoolId,
Filter = $"family_name = \"{familyName}\""
};
var response = await _cognitoClient.ListUsersAsync(request).ConfigureAwait(false);
_logger.LogInformation("Users with family_name {FamilyName} retrieved successfully from Cognito.", familyName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while retrieving users with family_name {FamilyName} from Cognito.", familyName);
throw;
}
}
Searching with more than one filter condition:
// search all users with certain email and family_name
public async Task<ListUsersResponse> ListUsersAsync(string email, string familyName)
{
try
{
var request = new ListUsersRequest
{
UserPoolId = _userPoolId,
Filter = $"email = \"{email}\" and family_name = \"{familyName}\""
};
var response = await _cognitoClient.ListUsersAsync(request).ConfigureAwait(false);
_logger.LogInformation("Users with email {Email} and family_name {FamilyName} retrieved successfully from Cognito.", email, familyName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while retrieving users with email {Email} and family_name {FamilyName} from Cognito.", email, familyName);
throw;
}
}
We can also search for all the users by not specifying any filter:
// Fetch all users in a pool
public async Task<ListUsersResponse> ListUsersAsync()
{
try
{
var request = new ListUsersRequest
{
UserPoolId = _userPoolId
};
var response = await _cognitoClient.ListUsersAsync(request).ConfigureAwait(false);
_logger.LogInformation("All users retrieved successfully from Cognito.");
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while retrieving all users from Cognito.");
throw;
}
}
Updating User Attributes
To update user attributes in Cognito, we send a request with the user’s username and the attributes we want to modify. Cognito allows updating standard attributes like email, phone number, and name, as well as any custom attributes that were configured in the user pool. The update applies immediately, and users might need to verify certain attributes depending on the pool settings.
Below is an example method that updates user attributes in Cognito:
public async Task<AdminUpdateUserAttributesResponse> UpdateUserAttributesAsync(string username, Dictionary<string, string> attributes)
{
try
{
var request = new AdminUpdateUserAttributesRequest
{
UserPoolId = _userPoolId,
Username = username,
UserAttributes = attributes.Select(kvp => new AttributeType { Name = kvp.Key, Value = kvp.Value }).ToList()
};
var response = await _cognitoClient.AdminUpdateUserAttributesAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} attributes updated successfully in Cognito.", username);
return response;
}
catch (UserNotFoundException ex)
{
_logger.LogWarning(ex, "Attempted to update attributes for non-existent user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while updating attributes for user {Username} in Cognito.", username);
throw;
}
}
Settings User Password
To reset a user’s password in Cognito, we use the AdminResetUserPassword API. This forces the user to set a new password the next time they log in. The method requires the user pool ID and the username of the account.
// Reset user password
public async Task<AdminResetUserPasswordResponse> ResetUserPasswordAsync(string username)
{
try
{
var request = new AdminResetUserPasswordRequest
{
UserPoolId = _userPoolId,
Username = username
};
var response = await _cognitoClient.AdminResetUserPasswordAsync(request).ConfigureAwait(false);
_logger.LogInformation("Password reset successfully for user {Username}.", username);
return response;
}
catch (UserNotFoundException ex)
{
_logger.LogWarning(ex, "Attempted to reset password for non-existent user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while resetting password for user {Username}.", username);
throw;
}
}
Disabling Users
To disable a user in Cognito, we use the AdminDisableUser API. This prevents the user from logging in while keeping their account in the user pool. The method requires the user pool ID and the username of the account. Once disabled, any authentication attempts by the user will be rejected until the account is re-enabled.
// Disable user
public async Task<AdminDisableUserResponse> DisableUserAsync(string username)
{
try
{
var request = new AdminDisableUserRequest
{
UserPoolId = _userPoolId,
Username = username
};
var response = await _cognitoClient.AdminDisableUserAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} disabled successfully in Cognito.", username);
return response;
}
catch (UserNotFoundException ex)
{
_logger.LogWarning(ex, "Attempted to disable non-existent user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while disabling user {Username} in Cognito.", username);
throw;
}
}
User Authentication
In this section, we’ll explore two methods for user authentication in AWS Cognito: ADMIN_USER_PASSWORD_AUTH and USER_PASSWORD_AUTH. Both methods involve submitting a user’s credentials, but they differ in their intended use cases and security considerations.
ADMIN_USER_PASSWORD_AUTH
This flow is designed for server-side applications where the backend handles user authentication. It allows the server to directly send the user’s username and password to AWS Cognito for authentication. This approach is suitable when the server is trusted to manage user credentials securely.
public async Task<AdminInitiateAuthResponse> SignInUserAsync(string username, string password)
{
try
{
var request = new AdminInitiateAuthRequest
{
UserPoolId = _userPoolId,
ClientId = _clientId,
AuthFlow = AuthFlowType.ADMIN_USER_PASSWORD_AUTH,
AuthParameters = new Dictionary<string, string>
{
{ "USERNAME", username },
{ "PASSWORD", password },
{ "SECRET_HASH", GetSecretHash(username, _clientId, _clientSecret) }
}
};
var response = await _cognitoClient.AdminInitiateAuthAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} signed in successfully.", username);
return response;
}
catch (NotAuthorizedException ex)
{
_logger.LogWarning(ex, "Invalid credentials for user {Username}.", username);
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while signing in user {Username}.", username);
throw;
}
}
private static string GetSecretHash(string username, string clientId, string clientSecret)
{
var secretBlock = username + clientId;
var keyBytes = Encoding.UTF8.GetBytes(clientSecret);
using (var hmac = new HMACSHA256(keyBytes))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(secretBlock));
return Convert.ToBase64String(hash);
}
}
USER_PASSWORD_AUTH
This flow is suitable for client-side applications where the client securely collects user credentials and sends them to Cognito for authentication. It’s essential to ensure that the client application can securely handle and transmit user credentials to prevent potential security risks.
public async Task<InitiateAuthResponse> SignInUserAsyncWithPasswordAuth(string username, string password)
{
var authRequest = new InitiateAuthRequest
{
AuthFlow = AuthFlowType.USER_PASSWORD_AUTH,
ClientId = _clientId,
AuthParameters = new Dictionary<string, string>
{
{ "USERNAME", username },
{ "PASSWORD", password },
{ "SECRET_HASH", CalculateSecretHash(username) }
}
};
try
{
var authResponse = await _cognitoClient.InitiateAuthAsync(authRequest);
return authResponse;
}
catch (NotAuthorizedException)
{
Console.WriteLine("The username or password is incorrect.");
throw;
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred: {ex.Message}");
throw;
}
}
private string CalculateSecretHash(string username)
{
var key = Encoding.UTF8.GetBytes(_clientSecret);
using (var hmac = new HMACSHA256(key))
{
var message = Encoding.UTF8.GetBytes(username + _clientId);
var hash = hmac.ComputeHash(message);
return Convert.ToBase64String(hash);
}
}
Key differences between these two approaches:
- ADMIN_USER_PASSWORD_AUTH: Designed for server-side authentication. Here, your backend server securely handles user credentials and communicates directly with AWS Cognito.
- USER_PASSWORD_AUTH: Meant for client-side applications. In this case, the client application (like a mobile or web app) collects user credentials and sends them to AWS Cognito for authentication.
API Methods:
- ADMIN_USER_PASSWORD_AUTH: Utilizes the
AdminInitiateAuth
API, which requires AWS administrative credentials. This method is suitable for scenarios where the server manages authentication. - USER_PASSWORD_AUTH: Uses the
InitiateAuth
API, which doesn’t require administrative credentials. This method is appropriate for client applications handling authentication directly.
Security Considerations:
- ADMIN_USER_PASSWORD_AUTH: User credentials are processed securely on the server side, reducing the risk of exposure.
- USER_PASSWORD_AUTH: User credentials are handled on the client side, necessitating robust security measures to protect this sensitive information during transmission.
User sign out
To sign out a from Cognito, we simply need to call AdminUserGlobalSignOutAsync
and provide the username and pool Id:
public async Task<AdminUserGlobalSignOutResponse> SignOutUserAsync(string username)
{
try
{
var request = new AdminUserGlobalSignOutRequest
{
UserPoolId = _userPoolId,
Username = username
};
var response = await _cognitoClient.AdminUserGlobalSignOutAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} signed out successfully.", username);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while signing out user {Username}.", username);
throw;
}
}
Configuring Multi-Factor Authentication
To configure Multi-Factor Authentication (MFA) for a user in AWS Cognito, you can use the AdminSetUserMFAPreferenceRequest API. This allows you to enable or disable MFA options such as SMS, email, or authenticator apps (TOTP) for a specific user. It’s important to note that only one MFA factor can be set as preferred, and this preferred factor will be used during user authentication if multiple factors are activated. To enable Time-Based One-Time Password (TOTP) MFA for a user and set it as their preferred MFA method, you can make the following API request:
public async Task<AdminSetUserMFAPreferenceResponse> EnableMFAForUserAsync(string username)
{
try
{
var request = new AdminSetUserMFAPreferenceRequest
{
UserPoolId = _userPoolId,
Username = username,
// set MFA preference to 'SOFTWARE_TOKEN_MFA'
SMSMfaSettings = new SMSMfaSettingsType { Enabled = false },
SoftwareTokenMfaSettings = new SoftwareTokenMfaSettingsType { Enabled = true, PreferredMfa = true }
};
var response = await _cognitoClient.AdminSetUserMFAPreferenceAsync(request).ConfigureAwait(false);
_logger.LogInformation("MFA enabled successfully for user {Username}.", username);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while enabling MFA for user {Username}.", username);
throw;
}
}
in this request, SoftwareTokenMfaSettings
is an object where:
Enabled
set totrue
activates TOTP MFA for the user.PreferredMfa
set totrue
designates TOTP as the user’s preferred MFA method.
Note: Before enabling Multi-Factor Authentication (MFA) for a user, ensure that all necessary prerequisites are met:
- SMS-based MFA: The user’s phone number must be added to their profile.
- Email-based MFA: The user’s email address must be added to their profile.
- TOTP (Time-Based One-Time Password) MFA: The user needs to register an authenticator application.
Additionally, the overall MFA settings of your user pool can influence individual user MFA preferences. It’s essential to configure these settings appropriately to ensure a seamless MFA experience for your users.
Managing Groups in Cognito
AWS Cognito allows managing users in groups, making it easier to control permissions and access levels. Groups can be used to assign different roles, such as Admins, Editors, or Viewers, and can help enforce role-based access control (RBAC) in your application.
Below are the key operations for managing groups in Cognito, including creating and deleting groups, as well as adding and removing users from them.
Creating a group
To create a new group in Cognito, we provide a group name and an optional description. The method sends a request to Cognito and logs a success message if the group is created successfully.
public async Task<CreateGroupResponse> CreateGroupAsync(string groupName, string description)
{
try
{
var request = new CreateGroupRequest
{
UserPoolId = _userPoolId,
GroupName = groupName,
Description = description
};
var response = await _cognitoClient.CreateGroupAsync(request).ConfigureAwait(false);
_logger.LogInformation("Group {GroupName} created successfully in Cognito.", groupName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while creating group {GroupName} in Cognito.", groupName);
throw;
}
}
Adding a User to a Group
To assign a user to a specific group, we send a request with the username and the group name. This allows us to control the user’s permissions within the application based on the group they belong to.
public async Task<AdminAddUserToGroupResponse> AddUserToGroupAsync(string username, string groupName)
{
try
{
var request = new AdminAddUserToGroupRequest
{
UserPoolId = _userPoolId,
Username = username,
GroupName = groupName
};
var response = await _cognitoClient.AdminAddUserToGroupAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} added to group {GroupName} successfully in Cognito.", username, groupName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while adding user {Username} to group {GroupName} in Cognito.", username, groupName);
throw;
}
}
Removing a User from a Group
If a user no longer needs access to a group’s permissions, we can remove them using this method. It requires both the username and the group name.
public async Task<AdminRemoveUserFromGroupResponse> RemoveUserFromGroupAsync(string username, string groupName)
{
try
{
var request = new AdminRemoveUserFromGroupRequest
{
UserPoolId = _userPoolId,
Username = username,
GroupName = groupName
};
var response = await _cognitoClient.AdminRemoveUserFromGroupAsync(request).ConfigureAwait(false);
_logger.LogInformation("User {Username} removed from group {GroupName} successfully in Cognito.", username, groupName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while removing user {Username} from group {GroupName} in Cognito.", username, groupName);
throw;
}
}
Deleting a Group
We can delete a group by providing the group name if it is no longer needed. This method ensures the group is correctly removed from Cognito.
public async Task<DeleteGroupResponse> DeleteGroupAsync(string groupName)
{
try
{
var request = new DeleteGroupRequest
{
UserPoolId = _userPoolId,
GroupName = groupName
};
var response = await _cognitoClient.DeleteGroupAsync(request).ConfigureAwait(false);
_logger.LogInformation("Group {GroupName} deleted successfully in Cognito.", groupName);
return response;
}
catch (Exception ex)
{
_logger.LogError(ex, "Unexpected error while deleting group {GroupName} in Cognito.", groupName);
throw;
}
}
Multi-Tenancy in Cognito with .NET
Multi-tenancy is a software architecture where a single instance of an application serves multiple customers, known as tenants. Each tenant’s data and configurations are isolated, ensuring privacy and security within a shared environment. This approach contrasts with single-tenancy, where each customer has a separate software instance.
In a multi-tenant setup, tenants can customize certain parts of the application, such as user interfaces or business rules, without altering the application’s core code. This architecture is fundamental to cloud computing, enabling efficient resource utilization and cost savings by allowing multiple customers to share the same infrastructure.
Understanding Multi-Tenancy Models in Cognito
1. Separate User Pools for Each Tenant
In this approach, each tenant gets its own user pool.
Pros:
- Isolation: Each tenant’s data and configurations are completely separate, enhancing security.
- Customization: Allows tenant-specific settings, such as password policies and MFA configurations.
Cons:
- Management Overhead: Maintaining multiple user pools can be complex and may require additional automation.
2. Groups within a Single User Pool
Here, a single user pool is used, with each tenant represented by a distinct group.
Pros:
- Simplified Management: All users are managed within one user pool, reducing administrative complexity.
- Efficient Resource Usage: Avoids the overhead of multiple user pools.
Cons:
- Limited Customization: Global settings apply to all tenants, limiting tenant-specific configurations.
- Potential Security Risks: Improper group management could lead to data exposure between tenants.
3. Custom Attributes for Tenant Identification
This method involves adding a custom attribute (e.g., tenantId
) to each user’s profile to indicate their tenant association.
Pros:
- Flexibility: Allows dynamic assignment and management of tenant associations.
- Scalability: Easily accommodates a growing number of tenants without significant changes to the user pool structure.
Cons:
- Complex Authorization Logic: Requires additional application logic to enforce tenant-based access controls.
- Risk of Misconfiguration: Incorrect handling of custom attributes could lead to unauthorized data access.
Implementing Multi-Tenancy in .NET with custom attributes
Using Custom Attributes for Tenant Isolation
Assign a unique tenantId
to each user during registration:
var signUpRequest = new SignUpRequest
{
ClientId = _clientId,
Username = username,
Password = password,
UserAttributes = new List<AttributeType>
{
new AttributeType
{
Name = "custom:tenantId",
Value = tenantId
},
// Additional attributes like email, phone number, etc.
}
};
var response = await _cognitoClient.SignUpAsync(signUpRequest);
Restricting Users Based on Their Tenant
During authentication, retrieve the tenantId from the user’s attributes to enforce tenant-specific access controls:
var authRequest = new AdminInitiateAuthRequest
{
UserPoolId = _userPoolId,
ClientId = _clientId,
AuthFlow = AuthFlowType.ADMIN_NO_SRP_AUTH,
AuthParameters = new Dictionary<string, string>
{
{ "USERNAME", username },
{ "PASSWORD", password }
}
};
var authResponse = await _cognitoClient.AdminInitiateAuthAsync(authRequest);
// Extract tenantId from user attributes
var userRequest = new AdminGetUserRequest
{
UserPoolId = _userPoolId,
Username = username
};
var userResponse = await _cognitoClient.AdminGetUserAsync(userRequest);
var tenantId = userResponse.UserAttributes.FirstOrDefault(attr => attr.Name == "custom:tenantId")?.Value;
// Use tenantId to apply tenant-specific logic
Conclusion
Managing user authentication and security in your .NET applications can be challenging, but integrating AWS Cognito simplifies the process. By following this guide, you’ve learned how to set up user pools, handle user accounts, and manage groups effectively.
Thank you for taking the time to read this post. I hope I was able to provide some guidance to make it easy and simple for you to work with AWS Cognito in .NET.
Cover Photo by Markus Spiske