MediumAWS Cognito: From “What Is This?” to a Working Login | by Kunal Rathod | Sep, 2026 | Medium
Sitemap
Kunal Rathod

Kunal Rathod

AWS Cognito: From “What Is This?” to a Working Login

16 min readSep 29, 2026

--

User pools, JWTs, PKCE, refresh tokens, and the Java code behind a real authentication flow.

Press enter or click to view image in full size

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_HASH work
  • 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
S3

If 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 / Deny

The identity pool is a separate path:

    Cognito Token
|
v
┌─────────────────┐
│ Identity Pool │
└────────┬────────┘
│
│ Temporary AWS credentials
v
S3 / DynamoDB
/ Other AWS APIs

So 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 attributes

For 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:groups

This 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.json

Your 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:

  • Email
  • 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.com

Or configure a custom domain such as:

https://auth.example.com

One 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 API

Let’s break that down.

Step 1: Generate the PKCE values

Your application generates:

  • code_verifier
  • code_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=S256

Step 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/token

Cognito 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/callback
Sign-out URL:
http://localhost:3000
OAuth grant:
Authorization code
Scopes:
openid
email
profile
Client secret:
Enabled

The callback URL is especially important.

This:

http://localhost:3000/callback

is 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_AbCdEfGhI

The Cognito hosted domain is different:

https://your-app.auth.ap-south-1.amazoncognito.com

Don’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
state

Your 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
Cognito

10. 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:

NotAuthorizedException

Also, 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:

  1. Delete its local session
  2. Clear the application cookie
  3. 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 session

Once 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 email

For API requests, the important distinction is:

token_use = access

Don’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

  • Google
  • Apple
  • Facebook
  • 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
groups

into 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. S3

The 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 enabled

Step 2

Create one public app client.

Use:

No client secret
Authorization Code grant

Step 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/callback

and:

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 = access

5. 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/groups

as 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 / DynamoDB

Or, 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.

--

--