Cognito from .NET
Amazon Cognito handles user authentication. Sign up, sign in, MFA, token issuance. It's AWS's managed auth service, roughly comparable to Auth0 or Azure AD B2C. You get a user pool (the user directory), an app client (your application's credentials), and JWT tokens that your API validates.
Cognito now has three feature plans (Lite, Essentials, Plus) with Essentials as the default. It includes the new Managed Login UI, passwordless authentication, and multi-region replication. The .NET integration story has improved but still isn't as well-documented as other languages. Here's how it works from the API side.
Architecture Overview
Typical flow:
- Client (browser/mobile) authenticates directly with Cognito β gets JWT tokens
- Client sends access token in
Authorizationheader - Your .NET API validates the JWT and extracts claims
- Authorization decisions based on claims (groups, custom attributes, scopes)
Your API never handles passwords. Cognito does that.
JWT Validation in ASP.NET Core
If you're running in a container (ECS, Fargate):
using Microsoft.AspNetCore.Authentication.JwtBearer;
builder.Services
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
var region = "us-east-1";
var userPoolId = "us-east-1_ABC123";
options.Authority = $"https://cognito-idp.{region}.amazonaws.com/{userPoolId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false, // Cognito access tokens don't include audience
ValidateLifetime = true,
ValidIssuer = $"https://cognito-idp.{region}.amazonaws.com/{userPoolId}",
};
});
Note: Cognito access tokens don't include an aud (audience) claim. Only ID tokens do. Set ValidateAudience = false for access token validation, or validate the client_id claim manually.
JWT Validation in Lambda
For Lambda functions behind API Gateway, you have two options:
Option 1: API Gateway JWT Authorizer (recommended)
Let API Gateway validate the token before your Lambda even runs. No code needed in your function. The claims arrive in the request context:
public async Task<APIGatewayHttpApiV2ProxyResponse> Handler(
APIGatewayHttpApiV2ProxyRequest request, ILambdaContext context)
{
// Claims are already validated by API Gateway
var userId = request.RequestContext.Authorizer.Jwt.Claims["sub"];
var groups = request.RequestContext.Authorizer.Jwt.Claims["cognito:groups"];
// Your logic here
}
Option 2: Manual validation in Lambda
If you need more control (custom validation logic, caching JWKS):
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.IdentityModel.Protocols;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
public class TokenValidator
{
private static readonly ConfigurationManager<OpenIdConnectConfiguration> _configManager;
private static readonly TokenValidationParameters _validationParams;
static TokenValidator()
{
var region = Environment.GetEnvironmentVariable("AWS_REGION")!;
var userPoolId = Environment.GetEnvironmentVariable("USER_POOL_ID")!;
var issuer = $"https://cognito-idp.{region}.amazonaws.com/{userPoolId}";
_configManager = new ConfigurationManager<OpenIdConnectConfiguration>(
$"{issuer}/.well-known/openid-configuration",
new OpenIdConnectConfigurationRetriever(),
new HttpDocumentRetriever());
_validationParams = new TokenValidationParameters
{
ValidIssuer = issuer,
ValidateAudience = false,
ValidateLifetime = true,
IssuerSigningKeyResolver = (token, securityToken, kid, parameters) =>
{
var config = _configManager.GetConfigurationAsync(CancellationToken.None).Result;
return config.SigningKeys;
},
};
}
public ClaimsPrincipal? ValidateToken(string token)
{
try
{
var handler = new JwtSecurityTokenHandler();
return handler.ValidateToken(token, _validationParams, out _);
}
catch (SecurityTokenException)
{
return null;
}
}
}
Working with Cognito Admin API
For server-side user management (creating users, adding to groups, custom attributes):
using Amazon.CognitoIdentityProvider;
using Amazon.CognitoIdentityProvider.Model;
var cognitoClient = new AmazonCognitoIdentityProviderClient();
// Create a user
await cognitoClient.AdminCreateUserAsync(new AdminCreateUserRequest
{
UserPoolId = userPoolId,
Username = email,
UserAttributes = new List<AttributeType>
{
new() { Name = "email", Value = email },
new() { Name = "email_verified", Value = "true" },
new() { Name = "custom:tenant_id", Value = tenantId },
},
DesiredDeliveryMediums = new List<string> { "EMAIL" },
});
// Add user to a group
await cognitoClient.AdminAddUserToGroupAsync(new AdminAddUserToGroupRequest
{
UserPoolId = userPoolId,
Username = email,
GroupName = "admins",
});
// Get user attributes
var user = await cognitoClient.AdminGetUserAsync(new AdminGetUserRequest
{
UserPoolId = userPoolId,
Username = email,
});
var tenantId = user.UserAttributes.First(a => a.Name == "custom:tenant_id").Value;
Multi-Tenant Patterns
For SaaS applications, use a custom attribute (custom:tenant_id) to associate users with tenants:
// Extract tenant from validated token
public static string GetTenantId(ClaimsPrincipal user)
{
return user.FindFirst("custom:tenant_id")?.Value
?? throw new UnauthorizedAccessException("No tenant claim found");
}
// Use in data access layer
public async Task<List<Order>> GetOrdersAsync(ClaimsPrincipal user)
{
var tenantId = GetTenantId(user);
// Query scoped to tenant
return await _repository.QueryAsync(
pk: $"TENANT#{tenantId}",
skPrefix: "ORDER#");
}
CDK Setup (C#)
using Amazon.CDK;
using Amazon.CDK.AWS.Cognito;
var userPool = new UserPool(this, "AppUserPool", new UserPoolProps
{
UserPoolName = "my-app-users",
SelfSignUpEnabled = true,
SignInAliases = new SignInAliases { Email = true },
AutoVerify = new AutoVerifiedAttrs { Email = true },
PasswordPolicy = new PasswordPolicy
{
MinLength = 12,
RequireLowercase = true,
RequireUppercase = true,
RequireDigits = true,
RequireSymbols = false,
},
AccountRecovery = AccountRecovery.EMAIL_ONLY,
CustomAttributes = new Dictionary<string, ICustomAttribute>
{
["tenant_id"] = new StringAttribute(new StringAttributeProps { Mutable = false }),
},
RemovalPolicy = RemovalPolicy.RETAIN,
});
// App client (no secret β for public clients like SPAs)
var appClient = userPool.AddClient("WebClient", new UserPoolClientOptions
{
AuthFlows = new AuthFlow
{
UserSrp = true,
},
OAuth = new OAuthSettings
{
Flows = new OAuthFlows { AuthorizationCodeGrant = true },
Scopes = new[] { OAuthScope.OPENID, OAuthScope.EMAIL, OAuthScope.PROFILE },
CallbackUrls = new[] { "https://myapp.com/callback" },
LogoutUrls = new[] { "https://myapp.com/logout" },
},
AccessTokenValidity = Duration.Hours(1),
IdTokenValidity = Duration.Hours(1),
RefreshTokenValidity = Duration.Days(30),
});
// Cognito domain for hosted UI
userPool.AddDomain("CognitoDomain", new UserPoolDomainOptions
{
CognitoDomain = new CognitoDomainOptions
{
DomainPrefix = "my-app-auth",
},
});
// Groups for RBAC
new CfnUserPoolGroup(this, "AdminGroup", new CfnUserPoolGroupProps
{
GroupName = "admins",
UserPoolId = userPool.UserPoolId,
Description = "Application administrators",
});
Tips
- Use the API Gateway JWT Authorizer when possible. It's faster (no Lambda cold start), cheaper (no invocation cost for invalid tokens), and simpler (no validation code to maintain).
- Don't store sensitive data in JWT claims. Tokens are signed but not encrypted. Custom attributes like
tenant_idare fine; secrets are not. - Cognito has hard limits. 40 calls/second for admin APIs (AdminCreateUser, etc.) by default. If you're bulk-importing users, request a limit increase or use the CSV import feature.
- Refresh tokens live longer than you think. Default is 30 days. If a user's permissions change, they'll still have valid tokens until they expire. For sensitive operations, re-validate against Cognito directly.
- Consider Cognito triggers for custom logic. Pre-signup validation, post-confirmation actions, pre-token-generation to add custom claims. These are Lambda functions invoked by Cognito at specific points in the auth flow.
Further Reading
- Amazon Cognito Developer Guide
- Verifying JWTs from Cognito
- Cognito User Pool Lambda triggers
- Cognito pricing
- AWS Cognito overview: feature plans, multi-region replication, Managed Login, and when to use Cognito vs alternatives
Looking for hands-on help? View my .NET on AWS services β