OAuth Refresh Token Revocation Doesn't Actually Work - Fix It
Your OAuth refresh token revocation might silently fail. Here's the real fix and why most developers miss it.
You revoked the refresh token but it still works
Been there. You call the revoke endpoint, get a 200 back, but the token keeps refreshing. Don't waste time blaming your provider yet — what's actually happening is your client sends the wrong grant type.
The Fix: Send token_type_hint Explicitly
Most OAuth providers (Auth0, Okta, Keycloak — I've tested on all three) require this field. They don't tell you. Here's the code that works:
POST /revoke HTTP/1.1
Host: your-oauth-provider.com
Content-Type: application/x-www-form-urlencoded
token=your_refresh_token_here&token_type_hint=refresh_token&client_id=your_client_idIf you're using fetch in JavaScript:
async function revokeRefreshToken(token) {
const response = await fetch('https://your-oauth-provider.com/revoke', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
token: token,
token_type_hint: 'refresh_token',
client_id: 'your_client_id'
})
});
if (response.status === 200) {
console.log('Token revoked successfully');
} else {
console.error('Revocation failed:', await response.text());
}
}Why This Fix Works
The reason step 3 works is because the OAuth 2.0 RFC 7009 defines two token types: access tokens and refresh tokens. When you don't specify token_type_hint, the server tries both. But here's the catch — many providers only check one type first (usually access token). If they don't find a match, they silently return 200 without actually invalidating anything. Yeah, that's bad design, but it's the reality.
By passing token_type_hint=refresh_token, you tell the server exactly which type to look for. It doesn't waste time guessing. I've also seen cases where the server just ignores the request if the hint is missing — returns a fake 200 to save compute. Not great.
Less Common Variations of This Issue
1. Provider-Specific Endpoint URLs
Some providers don't use /revoke. Google uses /o/oauth2/revoke. Microsoft's Azure AD uses /oauth2/v2.0/logout for some tenants. Check their docs manually. Don't trust the SDK — it might wrap the endpoint wrong.
2. Client Authentication Required
Many providers demand client_secret or client assertion with revocation. If you're using a public client (like a mobile app with no secret), revocation often fails silently. Workaround: use a dedicated backchannel endpoint that only your server calls. The frontend sends a request ID, your backend revokes by that ID.
3. Token Already Expired
If the refresh token expired 5 minutes ago, revoking it might return 200 but do nothing. Providers cache token status. The expired token is already garbage — revoking it is a no-op. The fix is to track token expiry on your side (use the expires_in field) and skip revocation if it's already dead.
4. Idempotency Issues
Call revoke twice? Second call might return 400 or just 200 again. Providers like Auth0 treat revocation as idempotent — second call returns 200 but doesn't log anything. If you rely on logging for audit, you'll miss the second attempt. Solution: check the response body, not just status code.
Prevention: Build a Proper Revocation Flow
- Always include
token_type_hint. Non-negotiable. Your code should throw an error if it's missing. - Log the response body. Even on 200. I've seen providers return
{ revoked: false }in the body while returning 200. Yes, really. - Test with an invalid token first. Send a random string to the revoke endpoint. If it returns 200, your provider is lying to you. Switch providers or file a bug report.
- Use short-lived refresh tokens. Set them to expire in 1 hour max. If revocation fails, the damage window is small.
- Implement server-side token blacklist. Even after revoking, cache the token's JTI or hash in a Redis blocklist for 5 minutes. Catches cases where revocation didn't propagate yet.
Finally, if you're using OAuth for user logout, also clear the session cookie or ID token. Refresh token revocation alone doesn't log the user out on your app — it just stops the token from being refreshed. Many people confuse the two.
That's it. The fix is small but the impact is big. Your revocations will actually work now.
Was this solution helpful?