AWS Cognito: From “What Is This?” to a Working Login
User pools, JWTs, PKCE, refresh tokens, and the Java code behind a real authentication flow.
If you can already wire an API Gateway route to a Lambda function and attach an IAM policy to a role, you know enough AWS to start using Cognito.
But Cognito has a way of making simple authentication feel more complicated than it really is.
The reason? Cognito is actually two different services hiding behind one name.
Most tutorials also jump straight into configuration without first explaining what each piece is doing. You end up creating user pools, app clients, domains, and tokens without understanding how they fit together.
This guide takes the opposite approach.
We’ll start with the mental model, follow a real browser login from beginning to end, and then implement that flow in a Spring Boot API.
By the end, you should understand:
- What a Cognito user pool actually does
- When you need an identity pool — and when you don’t
- The difference between the ID token and access token
- How Authorization Code + PKCE works
- How to validate Cognito JWTs in Spring Boot
- How refresh tokens and
SECRET_HASHwork - How logout, groups, federation, and MFA fit into the picture
- The mistakes that commonly waste hours during integration
- When Cognito may not be the right choice
First, Forget the AWS Console
Before clicking through the Cognito console, remember one simple idea:
A user pool answers “Who is this user?”
An identity pool answers “What temporary AWS permissions should this user get?”
That’s the foundation of Cognito.
User Pool: Authentication
A user pool is a managed user directory.
It handles things such as:
- Sign-up
- Sign-in
- Password reset
- MFA
- User attributes
- Social login
- Enterprise federation
- JWT issuance
Most applications that simply need users to log in to an application only need a user pool.
Think of it as an OpenID Connect identity provider that AWS manages for you.
Identity Pool: AWS Credentials
An identity pool solves a different problem.
It can take a token from a user pool, Google, SAML, or even a guest session and exchange it for temporary AWS credentials through:
sts:AssumeRoleWithWebIdentity
Why would you want that?
Imagine a browser that needs to upload a file directly to S3 using the logged-in user’s AWS permissions.
The flow becomes:
User
|
| signs in
v
Cognito User Pool
|
| tokens
v
Identity Pool
|
| temporary AWS credentials
v
S3If your frontend only talks to your own backend API, you usually don’t need an identity pool.
That’s an important distinction because many Cognito integrations become unnecessarily complicated by introducing identity pools too early.
The Cognito Picture You Should Keep in Your Head
Here’s the basic architecture:
┌─────────────────────┐
│ Browser │
└──────────┬──────────┘
│
1. Login
│
▼
┌─────────────────────┐
│ Cognito User │
│ Pool │
└──────────┬──────────┘
│
Issues JWT tokens
│
▼
┌─────────────────────┐
│ Your Backend │
│ / API Gateway │
└──────────┬──────────┘
│
Verify JWT + claims
│
▼
Allow / DenyThe identity pool is a separate path:
Cognito Token
|
v
┌─────────────────┐
│ Identity Pool │
└────────┬────────┘
│
│ Temporary AWS credentials
v
S3 / DynamoDB
/ Other AWS APIsSo remember:
User pools mint identity.
Identity pools mint AWS credentials.
They are not interchangeable.
The Three Tokens That Appear After Login
Once a user successfully signs in, Cognito can return three tokens.
They sound similar, but they have very different jobs.
1. ID Token
The ID token is a JWT about the user.
It can contain claims such as:
sub
email
email_verified
name
custom attributesFor example, your frontend might use it to answer:
“Who is currently signed in?”
But there’s an important rule:
Don’t use the ID token as your API authorization token.
2. Access Token
The access token is about what the application session is allowed to do.
It can contain:
client_id
scope
token_use: access
cognito:groupsThis is the token your API should use when authorizing requests.
A typical request looks like:
Authorization: Bearer <access_token>Your backend then validates the token and checks whether the caller has the required scope or group.
3. Refresh Token
The refresh token is different.
You don’t normally inspect it like a JWT.
Instead, the application sends it back to Cognito when the access token expires so Cognito can issue a new access token and ID token without requiring the user to log in again.
Treat refresh tokens as highly sensitive credentials.
For browser applications, an httpOnly cookie managed by your backend is safer than putting sensitive tokens into localStorage.
How Does Your API Know a JWT Is Real?
Cognito signs its JWTs using RS256.
The corresponding public keys are published through the JWKS endpoint:
https://cognito-idp.{region}.amazonaws.com/{userPoolId}/.well-known/jwks.jsonYour backend retrieves the public keys and verifies the JWT signature.
Then it validates the claims.
The important point is:
Cognito issues the token. Your API is responsible for deciding whether to trust it.
If you use API Gateway or an Application Load Balancer with the appropriate authentication configuration, AWS can perform some of this validation for you.
What Do You Actually Need to Create?
A basic Cognito setup has four important pieces.
1. User Pool
This is your user directory.
Choose how users sign in:
- Phone
- Username
Enable the attributes your application actually needs.
For example, email is a common sign-in alias.
Be careful with custom attributes. They can appear in tokens with a custom: prefix and can be awkward to remove later.
2. App Client
An app client represents an application using your user pool.
For example, you might have separate clients for:
- Web application
- Mobile application
- Backend application
Public clients such as SPAs and mobile applications must not contain a client secret.
A secret embedded in frontend JavaScript isn’t really secret.
Server-side applications can use a client secret.
Configure things such as:
- Callback URLs
- Sign-out URLs
- OAuth scopes
- OAuth flows
- Token lifetimes
For browser applications, the Authorization Code flow is the normal choice.
3. Cognito Domain
Managed login and OAuth endpoints need a domain.
You can use a Cognito domain such as:
https://your-app.auth.ap-south-1.amazoncognito.comOr configure a custom domain such as:
https://auth.example.comOne AWS detail that often surprises people:
The ACM certificate for a Cognito custom domain needs to be in us-east-1.
4. Sign-In Experience
Cognito provides managed login pages for things such as:
- Sign-in
- Sign-up
- MFA
- Password reset
If you need a completely custom login form, you can interact directly with Cognito’s APIs. The Secure Remote Password flow (USER_SRP_AUTH) avoids sending the password directly to your own server.
What Actually Happens When a User Logs In?
For a modern browser application, use:
Authorization Code + PKCE
Avoid the old implicit grant for new applications.
The flow looks like this:
Browser
|
| 1. Generate code_verifier
| + code_challenge
|
v
Cognito /authorize
|
| 2. User signs in
|
v
Cognito
|
| 3. Redirect with one-time code
|
v
Your callback
|
| 4. Exchange code + verifier
|
v
Cognito /oauth2/token
|
| 5. ID + Access + Refresh tokens
|
v
Your application
|
| 6. API calls
| Authorization: Bearer access_token
v
Your APILet’s break that down.
Step 1: Generate the PKCE values
Your application generates:
code_verifiercode_challenge
The challenge is the SHA-256 hash of the verifier.
Step 2: Redirect the user to Cognito
https://your-domain.auth.ap-south-1.amazoncognito.com/oauth2/authorize
?client_id=APP_CLIENT_ID
&response_type=code
&scope=openid+email
&redirect_uri=https://app.example.com/callback
&code_challenge=CHALLENGE
&code_challenge_method=S256Step 3: The user signs in
Cognito handles the authentication.
Step 4: Cognito redirects back
Your callback receives a one-time authorization code:
/callback?code=...&state=...Step 5: Exchange the code
Your application sends the code together with the original code_verifier to:
/oauth2/tokenCognito then returns the tokens.
Step 6: Call your API
The application sends:
Authorization: Bearer <access_token>The API validates the token before processing the request.
Why Does PKCE Exist?
Imagine someone steals the authorization code during the login process.
The code by itself should not be enough to obtain tokens.
That’s what PKCE protects against.
The attacker would also need the original code_verifier, which never left the application during the authorization step.
In short:
Authorization code + verifier = token exchange
Without the verifier, the stolen code is useless.
Connecting Cognito to a Spring Boot API
Now let’s turn the protocol into working Java code.
For this example, the Spring Boot API acts as the OAuth client.
The browser does not receive the client secret.
The backend performs the authorization-code exchange and maintains the application session.
This approach works well for a server-rendered application or a separate frontend communicating with a backend.
1. Add the Required Libraries
With Gradle:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-jose'
implementation platform('software.amazon.awssdk:bom:2.31.78')
implementation 'software.amazon.awssdk:cognitoidentityprovider'
}spring-security-oauth2-jose handles JWT verification using Cognito's public keys.The AWS SDK is useful for Cognito operations such as refreshing tokens.
The authorization redirect and token exchange themselves are ordinary HTTPS requests.
2. Configure the Cognito App Client
For this example:
Callback URL:
http://localhost:8080/api/v1/auth/callbackSign-out URL:
http://localhost:3000OAuth grant:
Authorization codeScopes:
openid
profileClient secret:
Enabled
The callback URL is especially important.
This:
http://localhost:3000/callbackis not the same as:
http://localhost:3000/callback/Cognito expects the configured redirect URI to match.
3. Keep Configuration Outside Your Code
For example:
app:
cognito:
region: ${COGNITO_REGION:ap-south-1}
user-pool-id: ${COGNITO_USER_POOL_ID}
client-id: ${COGNITO_CLIENT_ID}
client-secret: ${COGNITO_CLIENT_SECRET:}
issuer-uri: ${COGNITO_ISSUER_URI}
domain: ${COGNITO_DOMAIN}
redirect-uri: ${COGNITO_REDIRECT_URI:http://localhost:8080/api/v1/auth/callback}
logout-uri: ${COGNITO_LOGOUT_URI:http://localhost:3000}The issuer looks like:
https://cognito-idp.ap-south-1.amazonaws.com/ap-south-1_AbCdEfGhIThe Cognito hosted domain is different:
https://your-app.auth.ap-south-1.amazoncognito.comDon’t confuse the two.
4. Bind the Configuration
A Java record works nicely here:
@ConfigurationProperties(prefix = "app.cognito")
public record CognitoProperties(
String region,
String userPoolId,
String clientId,
String clientSecret,
String issuerUri,
String domain,
String redirectUri,
String logoutUri) {
}Enable it using:
@EnableConfigurationProperties(CognitoProperties.class)5. Create the Cognito Client
Token refresh uses Cognito’s InitiateAuth API.
@Configuration
@EnableConfigurationProperties(CognitoProperties.class)
public class CognitoConfig {
@Bean
CognitoIdentityProviderClient cognitoClient(
CognitoProperties properties) {
return CognitoIdentityProviderClient.builder()
.region(Region.of(properties.region()))
.credentialsProvider(
DefaultCredentialsProvider.create())
.build();
}
}The important AWS concept here is that the hosted login itself does not require AWS credentials.
When running on EC2, ECS, or EKS, the default credential provider can use the workload’s IAM role when the SDK needs to call AWS APIs.
6. Redirect the Browser to Cognito
Your login endpoint can simply redirect the browser.
@GetMapping("/login")
public void login(HttpServletResponse response) throws IOException {
response.sendRedirect(
cognitoAuthClient.buildAuthorizeUrl());
}The URL builder needs to generate and store both state and the PKCE verifier:
public String buildAuthorizeUrl() {
String state = UUID.randomUUID().toString();
String codeVerifier = generateCodeVerifier();
stateStore.save(state, codeVerifier);
return trimTrailingSlash(properties.domain())
+ "/oauth2/authorize"
+ "?client_id=" + encode(properties.clientId())
+ "&response_type=code"
+ "&scope=" + encode("openid email profile")
+ "&redirect_uri=" + encode(properties.redirectUri())
+ "&state=" + encode(state)
+ "&code_challenge_method=S256"
+ "&code_challenge="
+ encode(codeChallengeS256(codeVerifier));
}Generate the verifier using cryptographically secure random bytes:
private static String generateCodeVerifier() {
byte[] bytes = new byte[32];
new SecureRandom().nextBytes(bytes);
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(bytes);
}
private static String codeChallengeS256(String codeVerifier)
throws Exception {
byte[] hash = MessageDigest.getInstance("SHA-256")
.digest(
codeVerifier.getBytes(
StandardCharsets.US_ASCII));
return Base64.getUrlEncoder()
.withoutPadding()
.encodeToString(hash);
}Don’t Forget state
Store the state and verifier temporarily.
For a single local instance, a ConcurrentHashMap is enough.
For a multi-instance production application, use something shared such as a database or cache.
The stored state should have a short TTL and should be deleted immediately after it is consumed.
Get Kunal Rathod’s stories in your inbox
Join Medium for free to get updates from this writer.
Otherwise, you risk allowing replay.
7. Exchange the Authorization Code
Cognito redirects the user back with:
code
stateYour backend retrieves the stored verifier and sends it to Cognito.
public CognitoTokens exchangeAuthorizationCode(
String code,
String state) throws Exception {
String codeVerifier = stateStore.consume(state)
.orElseThrow(() ->
new IllegalStateException(
"Invalid or expired OAuth state"));
String body = "grant_type=authorization_code"
+ "&client_id=" + encode(properties.clientId())
+ "&code=" + encode(code)
+ "&redirect_uri="
+ encode(properties.redirectUri())
+ "&code_verifier=" + encode(codeVerifier);
if (properties.clientSecret() != null
&& !properties.clientSecret().isBlank()) {
body += "&client_secret="
+ encode(properties.clientSecret());
}
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create(
trimTrailingSlash(properties.domain())
+ "/oauth2/token"))
.header(
"Content-Type",
"application/x-www-form-urlencoded")
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
HttpResponse<String> response =
httpClient.send(
request,
HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200
|| response.statusCode() >= 300) {
throw new IllegalStateException(
"Token exchange failed: HTTP "
+ response.statusCode());
}
JsonNode json = objectMapper.readTree(response.body());
return new CognitoTokens(
json.get("id_token").asText(),
json.get("access_token").asText(),
json.get("refresh_token").asText());
}The redirect_uri must be exactly the same value used during authorization.
If it differs, Cognito will reject the exchange.
8. Validate the ID Token
Before using information such as the user’s email to create a local session, validate the token.
Spring Security can retrieve Cognito’s JWKS automatically:
private JwtDecoder jwtDecoder() {
NimbusJwtDecoder decoder =
(NimbusJwtDecoder)
JwtDecoders.fromIssuerLocation(
properties.issuerUri());
OAuth2TokenValidator<Jwt> withIssuer =
JwtValidators.createDefaultWithIssuer(
properties.issuerUri());
OAuth2TokenValidator<Jwt> audience = jwt -> {
if (jwt.getAudience() != null
&& jwt.getAudience()
.contains(properties.clientId())) {
return OAuth2TokenValidatorResult.success();
}
return OAuth2TokenValidatorResult.failure(
new OAuth2Error(
"invalid_token",
"Invalid audience",
null));
};
decoder.setJwtValidator(
new DelegatingOAuth2TokenValidator<>(
withIssuer,
audience));
return decoder;
}Then make sure you’re actually dealing with an ID token:
public Jwt validateIdToken(String idToken) {
Jwt jwt = jwtDecoder().decode(idToken);
if (!"id".equals(
jwt.getClaimAsString("token_use"))) {
throw new IllegalStateException(
"Expected an ID token");
}
return jwt;
}Read the email from the validated JWT, not directly from the token endpoint response.
Also, cache the decoder rather than rebuilding it on every login.
9. Create Your Own Application Session
After the ID token is validated, you can create your application’s session.
@GetMapping("/callback")
public void callback(
@RequestParam(required = false) String code,
@RequestParam(required = false) String state,
@RequestParam(required = false) String error,
HttpServletResponse response)
throws IOException {
if (error != null
|| code == null
|| state == null) {
response.sendRedirect(
"http://localhost:3000?error=login_failed");
return;
}
CognitoTokens tokens =
cognitoAuthClient
.exchangeAuthorizationCode(code, state);
Jwt idToken =
cognitoAuthClient
.validateIdToken(tokens.idToken());
String email =
idToken.getClaimAsString("email");
String sessionId =
sessionStore.create(email, tokens);
response.addHeader(
HttpHeaders.SET_COOKIE,
ResponseCookie.from(
"APP_SESSION",
sessionId)
.httpOnly(true)
.secure(true)
.path("/")
.sameSite("Lax")
.maxAge(Duration.ofDays(7))
.build()
.toString());
response.sendRedirect("http://localhost:3000");
}Notice what the browser receives:
APP_SESSION=<random-session-id>Not the refresh token.
The refresh token stays in your server-side session store.
That gives you a useful separation:
Browser
|
| random session ID
v
Your Backend
|
| refresh token
v
Cognito10. Refresh Expired Tokens
Access tokens are short-lived.
When one is close to expiry, your backend can use the refresh token to obtain new tokens.
If your app client has a secret, Cognito requires a SECRET_HASH.
public CognitoTokens refresh(
String refreshToken,
String previousIdToken) {
Map<String, String> authParams =
new HashMap<>();
authParams.put(
"REFRESH_TOKEN",
refreshToken);
if (properties.clientSecret() != null
&& !properties.clientSecret().isBlank()) {
authParams.put(
"SECRET_HASH",
secretHash(
cognitoUsername(previousIdToken)));
}
InitiateAuthResponse response =
cognitoClient.initiateAuth(
InitiateAuthRequest.builder()
.authFlow(
AuthFlowType
.REFRESH_TOKEN_AUTH)
.clientId(
properties.clientId())
.authParameters(authParams)
.build());
AuthenticationResultType result =
response.authenticationResult();
String nextRefresh =
result.refreshToken() != null
? result.refreshToken()
: refreshToken;
return new CognitoTokens(
result.idToken(),
result.accessToken(),
nextRefresh);
}The SECRET_HASH is:
Base64(
HMAC-SHA256(
clientSecret,
username + clientId
)
)In Java:
private String secretHash(String username)
throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(
new SecretKeySpec(
properties.clientSecret()
.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"));
byte[] raw =
mac.doFinal(
(username
+ properties.clientId())
.getBytes(
StandardCharsets.UTF_8));
return Base64.getEncoder()
.encodeToString(raw);
}And here’s the easy-to-miss part:
The username used for the hash comes from cognito:username.
It is not necessarily the user’s email address.
Using the email instead is a common cause of:
NotAuthorizedExceptionAlso, Cognito may not return a new refresh token every time.
If it doesn’t, keep the existing refresh token.
Always validate the newly returned ID token before replacing the tokens stored in the session.
11. Logout Means More Than Clearing Your Cookie
Deleting your application’s session is only half of logout.
The Cognito hosted session may still exist.
Build a logout URL:
public String buildLogoutUrl() {
return trimTrailingSlash(properties.domain())
+ "/logout"
+ "?client_id="
+ encode(properties.clientId())
+ "&logout_uri="
+ encode(properties.logoutUri());
}Your application should:
- Delete its local session
- Clear the application cookie
- Redirect to Cognito’s logout endpoint
The logout_uri must be configured as a sign-out URL on the app client.
Let’s Walk Through the Whole Flow
Once everything is connected, the entire login flow becomes surprisingly simple.
1. User opens
/api/v1/auth/login
↓
2. Backend generates
state + PKCE verifier
↓
3. Browser redirects to Cognito
↓
4. User signs in
↓
5. Cognito redirects to
/api/v1/auth/callback
↓
6. Backend exchanges
code + verifier
↓
7. Cognito returns tokens
↓
8. Backend validates
the ID token
↓
9. Backend creates
application session
↓
10. Browser receives
APP_SESSION cookie
↓
11. API requests use
the application session
↓
12. Backend refreshes
Cognito tokens when needed
↓
13. Logout clears the session
and ends the Cognito sessionOnce you see the complete picture, the individual Cognito settings start making much more sense.
What Should Your API Actually Check?
A JWT that looks correct isn’t automatically trustworthy.
For an API access token, verify at least:
- Signature — using Cognito’s current JWKS keys
- Expiration (
exp) - Token type (
token_use) - Issuer (
iss) - Client ID
- Scopes
For example:
iss
= https://cognito-idp.ap-south-1.amazonaws.com/ap-south-1_AbCdEfGhI
token_use
= access
client_id
= 1exampleclientid
exp
= future timestamp
scope
= openid emailFor API requests, the important distinction is:
token_use = accessDon’t simply decode the middle section of a JWT and trust the claims.
Anyone can create a token-shaped string.
The signature is what proves Cognito issued the token.
What Happens If You Use Google, SAML, or Another IdP?
Cognito doesn’t have to be the system where the user originally authenticates.
A user pool can federate with:
Social providers
- Apple
- Amazon
Enterprise providers
- SAML 2.0
- OIDC
- Workforce identity providers
The important part is what happens after federation.
The upstream provider authenticates the user, but your application can still receive Cognito tokens.
That means your API doesn’t need a completely different JWT validation strategy for every identity provider.
Your application can continue validating Cognito-issued tokens.
You can also map upstream attributes such as:
email
name
groupsinto Cognito user attributes.
For more advanced mappings, Cognito provides Lambda triggers such as the Pre Token Generation trigger.
Where Does MFA Fit?
Once the basic login flow is working, MFA is another layer you can enable.
Cognito supports options such as:
- TOTP authenticator applications
- SMS
An authenticator application avoids the delivery and cost considerations associated with SMS.
MFA can be configured according to your application’s requirements, including policies that apply to particular users or groups.
For privileged users, you may want stronger authentication requirements than for ordinary users.
So When Do You Actually Need an Identity Pool?
Come back to the original question:
“Does my application need temporary AWS credentials?”
If the answer is no, you probably don’t need an identity pool.
A common example where you do need one is direct browser-to-S3 uploads.
The flow becomes:
1. User signs in
↓
2. User Pool
↓
3. ID + Access tokens
↓
4. Identity Pool
↓
5. Temporary AWS credentials
↓
6. S3The credentials are temporary, and IAM determines what the user can access.
For example, you might restrict a user to their own S3 prefix:
arn:aws:s3:::uploads/${cognito-identity.amazonaws.com:sub}/*That’s very different from giving the identity:
s3:*on:
*The latter is the kind of shortcut that can turn a quick demo into a security incident.
If you don’t need unauthenticated guest access, leave it disabled.
The Simplest Cognito Project You Can Build
If you’re learning Cognito and want to understand the real flow without drowning in configuration, don’t start with every Cognito feature.
Build this:
Step 1
Create one user pool.
Use:
Email sign-in
Self-registration enabledStep 2
Create one public app client.
Use:
No client secret
Authorization Code grantStep 3
Create a Cognito domain.
Step 4
Enable managed login.
Step 5
Build a tiny frontend that redirects to Cognito.
Use a proper OIDC client library rather than manually implementing every part of PKCE if possible.
Step 6
Create one HTTP API with a JWT authorizer.
Point it at the user pool.
Step 7
Return something simple from the API:
{
"sub": "...",
"email": "..."
}Once this works, you understand the core Cognito flow.
Only then add things such as:
- Identity pools
- SAML
- Groups
- Lambda triggers
- Advanced MFA
- Direct AWS access
The Mistakes That Cost Developers Hours
Cognito isn’t necessarily difficult.
It’s just very easy to configure one small thing incorrectly.
Here are the mistakes worth knowing before you encounter them.
1. Creating an Identity Pool First
You get AWS credentials but still don’t have the authentication flow you actually needed.
Start with the user pool.
2. Putting a Client Secret in Frontend Code
A secret inside JavaScript isn’t secret.
For a SPA or mobile application, use a public client without a secret.
If the token exchange happens on your backend, a server-side client can use a secret.
3. Callback URL Mismatch
These are different:
http://localhost:3000/callbackand:
http://localhost:3000/callback/Make sure the URL configured in Cognito and the URL sent in the authorization request match exactly.
4. Using the ID Token to Authorize Your API
For API authorization, use the access token.
Check:
token_use = access5. Trusting the JWT Payload Without Verifying the Signature
Decoding a JWT is not validation.
Always verify:
signature
issuer
expiration
token type
audience/client ID
scopes/groupsas appropriate for your flow.
6. Making Your Tokens Huge
Every group, custom attribute, and scope you put into the token can end up being sent with API requests.
Keep tokens small.
If you have large profile information, retrieve it from your database using the Cognito sub.
7. Forgetting the Cognito Domain
Managed login won’t work until a domain exists.
And if you’re configuring a custom domain, remember the ACM certificate requirement for us-east-1.
8. Using One App Client Everywhere
Your web application, mobile application, and other clients don’t necessarily need to share the same app client.
Separate clients make it easier to manage:
- Credentials
- Redirect URLs
- Token settings
- Rotation
- Disabling one application independently
When Cognito Might Not Be the Right Fit
Cognito is a good fit for many AWS-based consumer and SaaS applications.
But authentication requirements vary.
You may want to reconsider Cognito when you need:
- A deeply customized authentication experience
- Complex account-linking workflows
- An existing mature workforce identity provider
- Authentication features that aren’t available in your selected Cognito tier
- Extensive identity customization that would require many Lambda triggers
For example, if your organization already uses a mature provider such as Okta, Microsoft Entra ID, or Auth0, you may be better served by federating that identity provider into Cognito or validating that provider’s tokens directly.
Running two separate user directories for the same users creates additional synchronization and lifecycle problems.
Also keep an eye on Cognito’s current feature tiers. Feature availability changes, so verify the current AWS documentation before designing an architecture around a specific feature.
A Production Checklist
Before calling your Cognito integration production-ready, walk through this list:
- Email verification enabled
- Production email configuration reviewed
- MFA available for privileged users
- Access tokens kept reasonably short-lived
- Refresh-token handling secured
- Authorization Code + PKCE used
- No client secret in public clients
- API validates issuer
- API validates audience or client ID
- API validates expiration
- API validates
token_use - Required scopes/groups are checked
- Identity-pool IAM policies use least privilege
- Cognito threat-protection/security features reviewed for your tier
- Logout ends both the application session and Cognito hosted session
- Secrets are stored outside source code
The One Mental Model to Remember
If you forget everything else in this article, remember this:
COGNITO
|
┌─────────┴─────────┐
| |
v v
USER POOL IDENTITY POOL
| |
| |
"Who are you?" "What AWS
| credentials
| do you get?"
| |
v v
JWTs AWS Credentials
| |
v v
Your API S3 / DynamoDBOr, in one sentence:
Use a user pool to authenticate users and give your application JWTs. Use an identity pool only when those users need temporary AWS credentials.
Once that distinction clicks, the rest of Cognito becomes configuration around two fairly simple paths.









