A mobile app’s users started getting logged out every hour, exactly on the hour, no matter how recently they’d signed in. The access token TTL had been set to 60 minutes during initial setup and never revisited, and the app had no refresh token flow implemented — it just assumed a session, once established, stayed valid. Cognito was expiring tokens exactly as configured. The “random logout bug” the team had been chasing for two sprints was a token lifetime setting that had been correct from day one and a client that never checked for it.
Cognito errors are precise about which token, which client, and which trigger failed — but the error names are often generic enough (NotAuthorizedException covers a lot of ground) that you need to read the accompanying message, not just the exception type.
The Problem
Sign-in, token refresh, or authorization fails in a handful of recognizable patterns:
| Symptom | What It Means |
|---|---|
NotAuthorizedException: Incorrect username or password |
Actually incorrect credentials — but also returned for a disabled/unconfirmed user |
NotAuthorizedException: Invalid Refresh Token |
Refresh token expired, revoked, or app client doesn’t have refresh token auth enabled |
| Token validates locally but API Gateway/backend rejects it | Wrong token type used (ID token vs access token) for the authorizer’s expectations |
| Users logged out unexpectedly | Access/ID token TTL shorter than assumed, with no silent refresh implemented client-side |
| Sign-up succeeds but user can’t log in | Pre-sign-up or post-confirmation Lambda trigger failed silently, leaving the user in an inconsistent state |
UserLambdaValidationException |
A Lambda trigger explicitly rejected the request — check the trigger’s own logs, not just Cognito’s |
Most of these are Cognito accurately reporting a state that the application layer didn’t anticipate.
Why Does This Happen?
- Confusing ID tokens and access tokens: ID tokens carry identity claims (for the application to read who the user is); access tokens are meant for authorizing API calls and carry scopes. An API Gateway Cognito authorizer expects the access token specifically — passing the ID token instead produces a rejection that looks like an auth failure but is actually a wrong-token-type error.
- Refresh token flow not enabled on the app client: Each app client independently controls which auth flows are permitted. If
ALLOW_REFRESH_TOKEN_AUTHisn’t enabled on the specific app client being used, silent token refresh fails even though the refresh token itself is valid. - Token expiration assumptions baked into client code: Access and ID token validity (default 60 minutes, configurable up to 24 hours) and refresh token validity (default 30 days) are independently configurable per app client — a client built assuming indefinite sessions will surface expiration as a mysterious logout bug rather than an expected refresh cycle.
- Lambda triggers failing without clear propagation: Pre-sign-up, post-confirmation, pre-authentication, and other Lambda triggers can throw exceptions that Cognito surfaces generically — the actual failure detail lives in the Lambda function’s own CloudWatch Logs, not in the Cognito-side error.
- User pool vs identity pool confusion: A user pool authenticates users and issues JWTs; an identity pool exchanges those JWTs for temporary AWS credentials. Using identity pool concepts (like IAM role mapping) to debug a pure user pool authentication failure, or vice versa, sends troubleshooting down the wrong path entirely.
- Clock skew or token audience mismatch during manual verification: When validating a JWT manually (outside of Amplify/the hosted SDKs), a missing
aud(audience/client ID) check, or excessive clock skew tolerance, can either wrongly accept or wrongly reject valid tokens.
The Fix
Step 1: Confirm Which Token Type Is Being Sent Where
Decode the JWT (without verifying signature, just to inspect claims) to check token_use:
echo "eyJraWQ..." | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool | grep token_use
Should read "token_use": "access" for API Gateway authorizers, or "id" for application-side identity claims. Mismatches here are a very common root cause.
Step 2: Check the App Client’s Enabled Auth Flows
aws cognito-idp describe-user-pool-client \
--user-pool-id us-east-1_AbCdEfGhI \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--query "UserPoolClient.ExplicitAuthFlows"
Should include ALLOW_REFRESH_TOKEN_AUTH and whichever sign-in flow you’re using (ALLOW_USER_SRP_AUTH, ALLOW_USER_PASSWORD_AUTH, etc.). If missing:
aws cognito-idp update-user-pool-client \
--user-pool-id us-east-1_AbCdEfGhI \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--explicit-auth-flows ALLOW_USER_SRP_AUTH ALLOW_REFRESH_TOKEN_AUTH
Step 3: Check Token Validity Periods Against Client Expectations
aws cognito-idp describe-user-pool-client \
--user-pool-id us-east-1_AbCdEfGhI \
--client-id 1a2b3c4d5e6f7g8h9i0j \
--query "UserPoolClient.{AccessTTL:AccessTokenValidity,IdTTL:IdTokenValidity,RefreshTTL:RefreshTokenValidity,Units:TokenValidityUnits}"
If the client-side application doesn’t implement silent refresh before expiry, either extend TTLs to match realistic session expectations, or (better) implement refresh-before-expiry logic using the refresh token rather than only extending TTLs as a workaround.
Step 4: Check the User’s Actual Status
aws cognito-idp admin-get-user \
--user-pool-id us-east-1_AbCdEfGhI \
--username jane.doe@example.com \
--query "{Status:UserStatus,Enabled:Enabled,UserAttributes:UserAttributes}"
UserStatus of UNCONFIRMED, FORCE_CHANGE_PASSWORD, or Enabled: false all produce the same generic NotAuthorizedException on sign-in attempts as a wrong password would.
Step 5: Check Lambda Trigger Logs for the Actual Failure
aws cognito-idp describe-user-pool \
--user-pool-id us-east-1_AbCdEfGhI \
--query "UserPool.LambdaConfig"
Identify which trigger is configured (e.g., PreSignUp, PostConfirmation), then check that specific function’s logs, not Cognito’s:
aws logs filter-log-events \
--log-group-name /aws/lambda/cognito-post-confirmation \
--start-time $(date -d '1 hour ago' +%s000) \
--filter-pattern "ERROR"
A trigger throwing an unhandled exception blocks the entire auth flow at that step — the fix is in the trigger’s code, not in Cognito configuration.
Step 6: Verify You’re Debugging the Right Pool Type
aws cognito-identity describe-identity-pool \
--identity-pool-id us-east-1:abc123-def456 \
--query "CognitoIdentityProviders"
Confirms which user pool(s) an identity pool trusts. If AWS credential exchange is failing (not sign-in itself), the problem is in the identity pool’s role mapping — a separate configuration surface from the user pool entirely.
aws cognito-identity get-identity-pool-roles \
--identity-pool-id us-east-1:abc123-def456
Step 7: Validate Token Signature and Claims Properly if Doing Manual Verification
Fetch the pool’s JWKS and verify signature, expiration, audience, and issuer explicitly:
curl -s "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_AbCdEfGhI/.well-known/jwks.json"
Confirm your verification library checks aud against the app client ID and iss against the expected user pool URL — skipping either check is a common source of accepting tokens meant for a different client or pool.
Is This Safe?
The describe, admin-get-user, and log queries are read-only. Updating ExplicitAuthFlows and token validity periods on an app client takes effect for new authentication requests without affecting already-issued tokens or active sessions. admin-get-user and other admin-* API calls require the calling principal to have appropriate IAM permissions scoped to the user pool — treat them as sensitive since they can expose user attribute data.
Key Takeaway
Cognito’s generic-sounding exceptions almost always have a specific, discoverable cause: decode the token to check which type is being used where, check the app client’s enabled auth flows and TTLs against what the client code actually assumes, and check the specific Lambda trigger’s own logs rather than trusting Cognito’s summary of a trigger failure. User pools and identity pools are separate systems solving separate problems — know which one you’re actually debugging before changing configuration.
Have questions or ran into a different Cognito issue? Connect with me on LinkedIn or X.