Skip to content

Commit 239e753

Browse files
token revocation endpoint support (fixes #49) (#62)
uses shared request parsing with parameter-based routing: `parseTokenEndpointRequest()` handles client authentication and body parsing once, then routes to either `handleRevocationRequest()` or `handleTokenRequest()` based on request parameters claude code was using in this PR --------- Co-authored-by: Sunil Pai <[email protected]>
1 parent 9cd9ab4 commit 239e753

File tree

3 files changed

+312
-76
lines changed

3 files changed

+312
-76
lines changed

.changeset/clear-cats-jump.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cloudflare/workers-oauth-provider': patch
3+
---
4+
5+
token revocation endpoint support

__tests__/oauth-provider.test.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2479,4 +2479,100 @@ describe('OAuthProvider', () => {
24792479
expect(clientsAfterDelete.items.length).toBe(0);
24802480
});
24812481
});
2482+
2483+
describe('Token Revocation', () => {
2484+
let clientId: string;
2485+
let clientSecret: string;
2486+
let redirectUri: string;
2487+
2488+
beforeEach(async () => {
2489+
redirectUri = 'https://client.example.com/callback';
2490+
2491+
// Create a test client
2492+
const clientResponse = await oauthProvider.fetch(
2493+
createMockRequest(
2494+
'https://example.com/oauth/register',
2495+
'POST',
2496+
{
2497+
'Content-Type': 'application/json',
2498+
},
2499+
JSON.stringify({
2500+
redirect_uris: [redirectUri],
2501+
client_name: 'Test Client for Revocation',
2502+
token_endpoint_auth_method: 'client_secret_basic',
2503+
})
2504+
),
2505+
mockEnv,
2506+
mockCtx
2507+
);
2508+
2509+
expect(clientResponse.status).toBe(201);
2510+
const client = await clientResponse.json<any>();
2511+
clientId = client.client_id;
2512+
clientSecret = client.client_secret;
2513+
});
2514+
2515+
it('should connect revokeGrant to token endpoint ', async () => {
2516+
// Step 1: Get tokens through normal OAuth flow
2517+
const authRequest = createMockRequest(
2518+
`https://example.com/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read&state=test-state`
2519+
);
2520+
const authResponse = await oauthProvider.fetch(authRequest, mockEnv, mockCtx);
2521+
const code = new URL(authResponse.headers.get('Location')!).searchParams.get('code');
2522+
2523+
const tokenRequest = createMockRequest(
2524+
'https://example.com/oauth/token',
2525+
'POST',
2526+
{
2527+
'Content-Type': 'application/x-www-form-urlencoded',
2528+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
2529+
},
2530+
`grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}`
2531+
);
2532+
2533+
const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx);
2534+
expect(tokenResponse.status).toBe(200);
2535+
const tokens = await tokenResponse.json<any>();
2536+
2537+
// Step 2:this should successfully revoke the token
2538+
const revokeRequest = createMockRequest(
2539+
'https://example.com/oauth/token',
2540+
'POST',
2541+
{
2542+
'Content-Type': 'application/x-www-form-urlencoded',
2543+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
2544+
},
2545+
`token=${tokens.access_token}`
2546+
);
2547+
2548+
const revokeResponse = await oauthProvider.fetch(revokeRequest, mockEnv, mockCtx);
2549+
// Verify response doesn't contain unsupported_grant_type error
2550+
const revokeResponseText = await revokeResponse.text();
2551+
expect(revokeResponseText).not.toContain('unsupported_grant_type');
2552+
2553+
// Step 3: Verify the access token is actually revoked
2554+
const apiRequest = createMockRequest('https://example.com/api/test', 'GET', {
2555+
Authorization: `Bearer ${tokens.access_token}`,
2556+
});
2557+
const apiResponse = await oauthProvider.fetch(apiRequest, mockEnv, mockCtx);
2558+
expect(apiResponse.status).toBe(401); // Access token should no longer work
2559+
2560+
// Step 4: Verify refresh token still works
2561+
const refreshRequest = createMockRequest(
2562+
'https://example.com/oauth/token',
2563+
'POST',
2564+
{
2565+
'Content-Type': 'application/x-www-form-urlencoded',
2566+
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
2567+
},
2568+
`grant_type=refresh_token&refresh_token=${tokens.refresh_token}`
2569+
);
2570+
2571+
const refreshResponse = await oauthProvider.fetch(refreshRequest, mockEnv, mockCtx);
2572+
expect(refreshResponse.status).toBe(200); // Refresh token should still work
2573+
const newTokens = await refreshResponse.json<any>();
2574+
expect(newTokens.access_token).toBeDefined();
2575+
expect(newTokens.refresh_token).toBeDefined();
2576+
});
2577+
});
24822578
});

0 commit comments

Comments
 (0)