fixed: gave access and refresh tokens distinct JWT header types #10

Merged
jak merged 1 commit from token-types into main 2026-10-23 16:50:51 -04:00
Owner

Every token had the header type JWT, and refresh-token claims include every field an access token requires. So a refresh token, and therefore an authorization code, decoded as an access token. A refresh token that had been rotated or revoked at logout could still be used as a bearer token at /userinfo (with the identity scope) and at its own app for the rest of its lifetime, 72 hours by default. It carried no capabilities, but it still identified the user.

Access tokens now use the header type at+jwt (RFC 9068) and refresh tokens rt+jwt. decodeToken requires the expected type. Authorization codes are still refresh tokens and share their type.

Breaking: integrations need the updated pkg/tokens / pkg/client. Tokens issued with the old header won't decode, so existing sessions must sign in again.

Tests:

  • New: TestToken_TypesAreNotInterchangeable (both directions, server and client validators) and TestGetUserInfo_RejectsRefreshTokenAndAuthorizationCode. The service test fails on main: userinfo accepts both.
  • Changed: three internal tests in pkg/tokens called helpers whose signatures now take a token type. TestVerifyHeader now expects plain JWT and the other token's type to be rejected. TestDecode_RejectsAudienceThatIsNotOneValue used to decode one token as both types. It now signs one token of each type with the same claims and checks the same audience errors.

🤖 Generated with Claude Code

Every token had the header type `JWT`, and refresh-token claims include every field an access token requires. So a refresh token, and therefore an authorization code, decoded as an access token. A refresh token that had been rotated or revoked at logout could still be used as a bearer token at `/userinfo` (with the `identity` scope) and at its own app for the rest of its lifetime, 72 hours by default. It carried no capabilities, but it still identified the user. Access tokens now use the header type `at+jwt` (RFC 9068) and refresh tokens `rt+jwt`. `decodeToken` requires the expected type. Authorization codes are still refresh tokens and share their type. **Breaking:** integrations need the updated `pkg/tokens` / `pkg/client`. Tokens issued with the old header won't decode, so existing sessions must sign in again. **Tests:** - New: `TestToken_TypesAreNotInterchangeable` (both directions, server and client validators) and `TestGetUserInfo_RejectsRefreshTokenAndAuthorizationCode`. The service test fails on `main`: userinfo accepts both. - Changed: three internal tests in `pkg/tokens` called helpers whose signatures now take a token type. `TestVerifyHeader` now expects plain `JWT` and the other token's type to be rejected. `TestDecode_RejectsAudienceThatIsNotOneValue` used to decode one token as both types. It now signs one token of each type with the same claims and checks the same audience errors. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Every token carried the header type 'JWT', and a refresh token's claims
included every field an access token requires. A refresh token, and so an
authorization code, decoded as an access token. That made a rotated or
logged-out refresh token usable as a bearer token at userinfo and at its app
until the refresh token expired.

We gave access tokens the header type 'at+jwt' and refresh tokens 'rt+jwt'.
Decoding requires the expected type. Integrations need the updated client
library, because tokens with the old header type no longer decode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
jak merged commit d702919292 into main 2026-10-23 16:50:51 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
pollinator/consent!10
No description provided.