import { CognitoIdentityProviderClient, RevokeTokenCommand } from '@aws-sdk/client-cognito-identity-provider'; /** * END THE SESSION AT COGNITO, NOT ONLY IN THIS BROWSER. * * Clearing storage stops THIS browser using the session. It does nothing about the session itself: the * refresh token stays valid for its full seven days, so anything holding a copy could keep minting fresh * access tokens long after somebody pressed Log Out. Revoking is what makes the sign-out real. * * WHY THE REFRESH TOKEN AND NOT THE ACCESS TOKEN. An access token is a stateless JWT - nothing can * invalidate one without asking a server on every request, and the rejected alternative was exactly that: * a revocation list the authorizer consults on every call in the product, forever, and a new way for the * authorizer to fail. Revoking the refresh token ends the ability to CONTINUE the session; the last access * token dies on its own inside its ~30 minute life and cannot be extended. * * WHY THE RESIDUAL WINDOW IS ACCEPTABLE, in the scenario it exists for: receptionist A signs out, B sits * down. Sign-out clears storage, so A's access token is not anywhere B can reach - the window only matters * to a token CAPTURED before sign-out, which means devtools or a proxy rather than a shift change. And our * tokens are DPoP-bound to the browser that minted them, so a captured one cannot be replayed elsewhere. * If the window ever needs to be smaller, shorten the token lifetime rather than adding architecture. * * `RevokeToken` RATHER THAN `GlobalSignOut`, deliberately. Revoking this refresh token ends THIS session * and every access token minted from it. A global sign-out would also end the person's session on their * phone, which is not what pressing Log Out at a front desk means. * * IT IS UNAUTHENTICATED AND TAKES NO CREDENTIAL OF OURS - the token being revoked is the credential, and * it is the one thing already in this browser and about to be deleted. So it needs no IAM grant, no server * route, and never sends a refresh token to any host but Cognito. */ export const revokeRefreshToken = async ( client: CognitoIdentityProviderClient, clientId: string, refreshToken: string | null, ): Promise => { if (!refreshToken || !clientId) return false; try { await client.send(new RevokeTokenCommand({ Token: refreshToken, ClientId: clientId })); return true; } catch (error) { /* * NEVER BLOCKS THE SIGN-OUT, and never surfaces. Somebody walking away from a shared machine must * not be held on the network, and the local half - storage cleared, page reloaded - has already * made the session unusable here whatever Cognito answered. * * The honest limit, stated rather than hidden: if this fails, the refresh token is still live for * its remaining days. It is logged so a pattern of failures is findable, because a revocation that * silently never works would look exactly like one that always does. */ console.error('Sign-out could not revoke the refresh token at Cognito:', error); return false; } };