The Target
A telehealth platform handling Protected Health Information. The platform issues two JWTs on login: an access token (8-hour expiry) and a refresh token (16-hour expiry). Both are HS256-signed and stored in sessionStorage.
The Discovery
After logging in, I decoded both tokens. The only differences were: the access token had origin: "web" and an 8-hour expiry, while the refresh token had origin: null and a 16-hour expiry. Same signing key, same user claims, same structure.
I tried using the refresh token as a Bearer token on the profile endpoint. It returned 200 with the full member profile. Then I tested it on every other endpoint. Prescriptions, visits, signed URLs, write operations. All worked. The server never checks which type of token it received.
The Proof
PHI read access: The refresh token returned 1,352 prescription records from the prescriptions endpoint, identical to what the access token returns. Full member profiles with medical history, addresses, and phone numbers also accessible.
Write access: Created a pending visit using the refresh token. The server accepted it, assigned a provider, and returned a visit ID. Also generated signed URLs that bypass Bearer authentication entirely.
No rotation: The refresh token is a static JWT with no rotation on use. Once issued, it stays valid for the full 16-hour window. No one-time-use enforcement.
Equal storage exposure: Both tokens are stored in sessionStorage via the same persistence mechanism. Any XSS would steal both. The refresh token gives an attacker 16 hours of full access versus 8 hours for the access token.
Impact
- Effective session lifetime doubled from 8 to 16 hours on a healthcare platform handling PHI
- Refresh token grants full read and write access to all endpoints, including prescriptions and visit creation
- No token type validation:
origin: nullaccepted identically toorigin: "web" - No token rotation, static JWT valid for entire 16-hour window
Timeline
Reported with initial findings, then followed up with expanded proof showing the refresh token works on PHI endpoints and write operations. Triaged as Medium. The team acknowledged the OAuth2 RFC 6749 violation and committed to implementing token type validation.
Takeaway
Always test whether refresh tokens are accepted on resource endpoints. Many implementations use the same JWT structure for both token types and only differentiate by expiry. If the server doesn't check a token_type claim or equivalent discriminator, the refresh token becomes a longer-lived access token. On platforms handling sensitive data like PHI, the difference between an 8-hour and 16-hour exposure window matters. The fix is straightforward: add a type claim to the JWT and reject refresh tokens on any endpoint except the token renewal endpoint.