Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/clear-cats-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@cloudflare/workers-oauth-provider': patch
---

token revocation endpoint support
81 changes: 81 additions & 0 deletions __tests__/oauth-provider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2268,4 +2268,85 @@ describe('OAuthProvider', () => {
expect(clientsAfterDelete.items.length).toBe(0);
});
});

describe('Token Revocation', () => {
let clientId: string;
let clientSecret: string;
let redirectUri: string;

beforeEach(async () => {
redirectUri = 'https://client.example.com/callback';

// Create a test client
const clientResponse = await oauthProvider.fetch(
createMockRequest(
'https://example.com/oauth/register',
'POST',
{
'Content-Type': 'application/json',
},
JSON.stringify({
redirect_uris: [redirectUri],
client_name: 'Test Client for Revocation',
token_endpoint_auth_method: 'client_secret_basic',
})
),
mockEnv,
mockCtx
);

expect(clientResponse.status).toBe(201);
const client = await clientResponse.json<any>();
clientId = client.client_id;
clientSecret = client.client_secret;
});

it('should connect revokeGrant to token endpoint ', async () => {
// Step 1: Get tokens through normal OAuth flow
const authRequest = createMockRequest(
`https://example.com/authorize?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=read&state=test-state`
);
const authResponse = await oauthProvider.fetch(authRequest, mockEnv, mockCtx);
const code = new URL(authResponse.headers.get('Location')!).searchParams.get('code');

const tokenRequest = createMockRequest(
'https://example.com/oauth/token',
'POST',
{
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
`grant_type=authorization_code&code=${code}&redirect_uri=${encodeURIComponent(redirectUri)}`
);

const tokenResponse = await oauthProvider.fetch(tokenRequest, mockEnv, mockCtx);
expect(tokenResponse.status).toBe(200);
const tokens = await tokenResponse.json<any>();

// Step 2:this should successfully revoke the token
const revokeRequest = createMockRequest(
'https://example.com/oauth/token',
'POST',
{
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
},
`token=${tokens.access_token}`
);

const revokeResponse = await oauthProvider.fetch(revokeRequest, mockEnv, mockCtx);
expect(revokeResponse.status).toBe(200); // Should NOT be unsupported_grant_type anymore

// Verify response doesn't contain unsupported_grant_type error
const revokeResponseText = await revokeResponse.text();
expect(revokeResponseText).not.toContain('unsupported_grant_type');

// Step 3: Verify the token is actually revoked (proves revokeGrant was called)
const apiRequest = createMockRequest('https://example.com/api/test', 'GET', {
Authorization: `Bearer ${tokens.access_token}`,
});
const apiResponse = await oauthProvider.fetch(apiRequest, mockEnv, mockCtx);
expect(apiResponse.status).toBe(401); // Token should no longer work
});
});
});
Loading