Business Challenge
The identity posts in this series have been about what a policy evaluates — #35 on the evaluation order, #70 on where a trust boundary actually sits. This one is about a credential that carries its own answer, so there is nothing left to evaluate.
AWS says it in one sentence, in the revocation documentation, and it is the whole post:
“User pool JWTs are self-contained with a signature and expiration time that was assigned when the token was created. Revoked tokens can't be used with any Amazon Cognito API calls that require a token. However, revoked tokens will still be valid if they are verified using any JWT library that verifies the signature and expiration of the token.”
Read the two halves of that sentence against how a token is normally checked. Local verification
— fetch the JWKS, check the RS256 signature, check exp, check aud and
iss — is the recommended pattern, it is what an API Gateway JWT authorizer does, and
it involves no call to Cognito at all. That is the point of a JWT.
Which means revocation changes nothing for that path. The signature is still good. exp is
still in the future. The library returns valid, correctly, because the token is valid by every
property it carries.
So "we revoked their token" and "they can no longer call our API" are two different statements, and only the first one is true at the moment you perform the revocation.
FixDecide deliberately whether your resource server checks revocation at all. If it must, that is an introspection call or a cache of revoked origin_jti values on the request path — and that is a real latency and availability decision, not a configuration toggle.
“cognito:groups An array of strings. Each string is the name of a user pool group that has your user as a member.”
An array, written into the token at issuance. Remove the user from the group and every token already in their browser still lists it, because a claim is a record of what was true when the token was signed.
The obvious mitigation is a short token life, and AWS allows a genuinely short one: “You can set the ID token expiration to any value between 5 minutes and 1 day”, per app client. Five minutes of stale authorisation is an acceptable exposure for most applications.
Except that it does not do what it looks like it does, which is the next card.
FixTreat group membership in a token as authorisation cached for the token's lifetime, and put anything that must revoke promptly behind a check your application performs rather than a claim it reads.
Important, in AWS's own callout: “When your user signs in with managed login, Amazon Cognito sets session cookies that are valid for 1 hour. If you use managed login for authentication in your application, and specify a minimum duration of less than 1 hour for your access and ID tokens, your users will still have a valid session until the cookie expires.”
And the part that closes the loop: “If the user has tokens that expire during the one-hour session, the user can refresh their tokens without the need to reauthenticate.”
So with managed login, a five-minute ID token buys you a token that is replaced every five minutes from a session that lasts an hour. Each replacement is freshly issued, which does help with group changes — and the session floor underneath it is the cookie, not the token.
FixSize the token TTL for how stale a claim may be, and understand the managed login cookie as the real minimum session length. They are different numbers answering different questions.
Architecture
Three clocks, one revocation identifier tying two of them to the third, and four operations with very different reach.
The three clocks
| Token | Configurable range | Default |
|---|---|---|
| ID token | “any value between 5 minutes and 1 day”, per app client | — |
| Access token | “any value between 5 minutes and 1 day” | — |
| Refresh token | “any value between 60 minutes and 10 years” | “expires 30 days after your application user signs into your user pool” |
Ten years is 3,650 days against a one-day ceiling on the other two, so the refresh token is the credential that actually determines how long an account stays usable. It is also the one that is least often reviewed, because it is invisible in normal operation — the thing in the browser that keeps working.
“Rotation reissues a refresh token within the same fixed validity window. It doesn't extend the window. Each new refresh token is valid only for the time remaining on the original RefreshTokenValidity period, no matter how many times the token rotates.”
With the worked example: “suppose you set RefreshTokenValidity to 30 days and a session rotates its refresh token every day. The session still expires 30 days after the first sign-in, not 30 days after the most recent rotation. To keep a user signed in past that window, they must reauthenticate.”
So rotation is a credential-theft control, not a session-extension mechanism. If you turned it on expecting sliding sessions, the session length did not change β and users will be signed out on the original schedule regardless of how active they were.
What origin_jti is for
“origin_jti A token-revocation identifier associated with your user's refresh token. Amazon Cognito references the origin_jti claim when it checks if you revoked your user's token with the Revoke endpoint or the RevokeToken API operation. When you revoke a token, Amazon Cognito invalidates all access and ID tokens with the same origin_jti value.”
That is a clean design: one identifier binds a refresh token to every access and ID token descended from it, so revoking the parent marks the whole family. The word doing the work is invalidates, and its scope is Cognito's own view — which is where the first challenge card came from.
It also tells you what to cache if you decide your API must honour revocation: origin_jti,
not the token.
Four operations, two of which cover a user
| Operation | Reach | Authorised by |
|---|---|---|
RevokeToken | One refresh token and its children. “doesn't affect any of the user's other refresh tokens” | Client ID plus the token |
| Revocation endpoint | “a given refresh token and all ID and access tokens that the refresh token generated” | Same |
GlobalSignOut | “all of the requesting user's refresh, ID, and access tokens” | The user's own access token |
AdminUserGlobalSignOut | “all of the target user's refresh, ID, and access tokens” | IAM credentials |
The first two are per-session. A user signed in on a laptop, a phone and a CI job has three refresh
tokens, and RevokeToken on one of them leaves the other two working — which is correct
for "sign out this device" and wrong for "this person has left".
For that, the operation is AdminUserGlobalSignOut, because it is the only one of the four an
administrator can perform against a user without holding that user's token. And note what
GlobalSignOut requires: “a user authorizes with their access token”
— so it is a self-service operation and not an offboarding tool.
One operational detail that will fail a first attempt: “Your request to revoke a refresh token must include the client ID that was used to obtain the token.” In a pool with several app clients, revoking means knowing which client issued the credential.
Why This Architecture Holds Up
Revocation state is one-way, which is the right choice
Two statements that together make the feature trustworthy:
“When you disable token revocation in an app client where it was previously enabled, revoked tokens don't become active again.”
“When you disable a user account (which revokes refresh and access tokens), the revoked tokens don't become active if you enable the user account again.”
Both close a reactivation path that would otherwise be a genuine hole — re-enabling a suspended account would have resurrected whatever was in the browser at suspension time. It is worth noting because the opposite behaviour is easy to implement by accident, and AWS went out of its way to document that it did not.
Enabling revocation changes the token, and the docs say so
“After you enable token revocation, new claims are added in the Amazon Cognito JSON Web Tokens. The origin_jti and jti claims are added to access and ID tokens. These claims increase the size of the application client access and ID tokens.”
A small thing with a real failure mode: tokens travel in headers, and headers have limits. A pool that turns on revocation and starts sending slightly larger JWTs through a proxy sized for the old ones has a change with no obvious cause. The remedy is to know it happened, which is all this paragraph is for.
Worth checking rather than assuming: “When you create a new user pool client, token revocation is enabled by default”, and “You can revoke refresh tokens only in app clients with token revocation enabled.” New clients are covered; a pool that predates the feature may have clients where the operation simply is not available, and the time to find that out is not during an incident.
“Refresh token rotation isn't compatible with the authentication flow REFRESH_TOKEN_AUTH”, and from the other direction: “You can't authenticate with REFRESH_TOKEN_AUTH in app clients with refresh token rotation enabled.”
REFRESH_TOKEN_AUTH through InitiateAuth is how a great deal of existing SDK code refreshes a session. Turning on rotation in an app client those SDKs use breaks that path, and the replacement is GetTokensFromRefreshToken. This is a client-code change, not a setting β so rotation is a migration rather than a toggle.
The compensating control for the race it introduces: “To allow for retries for a brief duration, you can also configure a grace period for the original refresh token of up to 60 seconds.” Which exists because two concurrent requests both refreshing at once is a real pattern, and without a grace period one of them loses.
With rotation off, the refresh token is long-lived and unchanging
“When this setting is disabled, token-refresh requests return new access and ID tokens only and the original refresh token remains valid.”
Combine that with a configured validity measured in months or years and the shape is clear: one stolen refresh token is a durable credential that mints working access tokens for as long as the window lasts, and nothing about using it looks anomalous. That is the case rotation exists for, and it is why the migration cost above is usually worth paying.
Two parsing cautions, both unusual to see documented
“The set of claims in a Amazon Cognito ID token grows over time as new features add new claims, so the claims that your tokens carry can differ from this example.” So a parser that rejects unexpected claims will eventually break on an AWS feature release rather than on a change of yours.
And: “Amazon Cognito generates sub in an Amazon Cognito-specific format that doesn't conform to a specific UUID format, including RFC UUID. You shouldn't strictly validate the format of sub.” A UUID-shaped string that is not a UUID, with an explicit instruction not to validate it as one. Worth obeying literally.
Key Architecture Decisions
| Decision | Choice | Reasoning |
|---|---|---|
| Whether your API honours revocation | Decide it explicitly, and write it down | Local JWT verification cannot see a revocation; AWS states revoked tokens “will still be valid” to a verifying library. |
| If it must honour it | Cache revoked origin_jti values, not tokens |
One origin_jti covers a refresh token and every child of it. |
| Offboarding a person | AdminUserGlobalSignOut |
The only operation an administrator can run against a user's whole token set with IAM credentials. |
| Signing out one device | RevokeToken or the revocation endpoint |
Scoped to one refresh token; other sessions are explicitly unaffected. |
| Group-based authorisation | Treat cognito:groups as cached for the token lifetime |
It is a claim fixed at issuance, so removal is not retroactive. |
| ID and access token TTL | Size it as "how stale may a claim be" | 5 minutes to 1 day, and it is the staleness bound, not the session length. |
| Managed login sessions | Treat the 1-hour cookie as the session floor | A shorter token still leaves “a valid session until the cookie expires”, refreshable without reauthentication. |
| Refresh token validity | Review it — it is the real session length | Default 30 days, configurable to 3,650; the one-day ceiling applies only to the other two. |
| Refresh token rotation | Enable it, and plan it as a client migration | It forecloses REFRESH_TOKEN_AUTH, so SDK code must move to GetTokensFromRefreshToken. |
| Rotation grace period | Set one, up to 60 seconds | Concurrent refreshes are normal, and without it one request loses. |
| Expecting rotation to extend sessions | Do not — it is explicitly fixed-window | “It doesn't extend the window”, with a worked 30-day example. |
| Token parsing | Tolerate unknown claims; do not validate sub as a UUID |
Claims “grows over time”, and sub is documented as not RFC-conformant. |
| Pre-existing app clients | Confirm EnableTokenRevocation before relying on it |
Default on for new clients only; revocation is unavailable where it was never enabled. |
The audit worth running
For each app client, write down four numbers and one boolean: ID token TTL, access token TTL, refresh token validity, rotation grace period, and whether token revocation is enabled. The refresh validity is the one most likely to be a default nobody chose, and it is the number that answers "how long can a departed employee's browser keep working".
Then answer the question this post exists for: when your API receives a token, does anything on that path consult Cognito? If the answer is no — and for a JWT authorizer it is no — then your revocation latency is exactly the access token TTL, whatever your runbook says.
Closing Thought
None of this is Cognito behaving badly. A self-contained token that can be verified without a network call
is the entire value proposition of JWTs, and the cost of that property is that nothing can be withdrawn
mid-flight. AWS implements the only revocation that is possible under those constraints, documents its
boundary in a single clear sentence, and provides origin_jti so you can build the rest
yourself if you need it.
The gap is linguistic rather than technical. "Revoke" and "remove from group" are verbs that sound instantaneous, and both are recorded instantly — in the user pool. Whether they take effect anywhere else is a property of the verifier, which is code you wrote and probably did not think of as part of your revocation story.
Which is a different shape from the four posts before it. #74, #75, #76 and #77 were all a number answering a question next to the one being asked. This one is an action taking effect in a system next to the one you meant — and it is the more dangerous variety, because a number that reads wrong gets argued about, while an action that reads done gets closed.
Security & Identity — Amazon Verified Permissions: what Cedar evaluates that an IAM policy cannot, why a policy store's schema is the thing that actually constrains authorisation, and how a deny is reached when no policy matches.
Official AWS Reference
- Revoking tokens and ending user sessions — the self-contained-JWT statement and the limit of revocation, the four operations and their reach, the client ID requirement, the enablement default, the added claims and their size effect, and the one-way nature of revocation state
- Understanding the identity (ID) token — the 5-minute-to-1-day range, the managed login one-hour session cookie, the
cognito:groupsandorigin_jticlaim definitions, RS256, and the two parsing cautions - Refresh tokens — the 60-minute-to-10-year range and 30-day default, refresh token rotation and its fixed validity window, the 60-second grace period, and the
REFRESH_TOKEN_AUTHincompatibility - Understanding the access token — the access token expiration range
- Using tokens with user pools — further reading on the token set as a whole; no claims in this post are drawn from it
Comments