Something went wrong. Try again.
AtAuth
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167import { describe, it, expect, beforeEach, afterEach } from 'vitest';import express from 'express';import request from 'supertest';import crypto from 'crypto';import { createRevokeRouter } from './revoke.js';import { DatabaseService } from '../../services/database.js';
function createTestApp(db: DatabaseService) { const app = express(); app.use(express.json()); app.use(express.urlencoded({ extended: true }));
const router = createRevokeRouter(db); app.use('/oauth', router);
return app;}
function registerOIDCClient(db: DatabaseService, id = 'test-client', secret = 'my-secret') { const secretHash = crypto.createHash('sha256').update(secret).digest('hex'); db.upsertOIDCClient({ id, name: 'Test App', client_type: 'oidc', hmac_secret: crypto.randomBytes(32).toString('hex'), client_secret: secretHash, redirect_uris: ['https://app.example.com/callback'], grant_types: ['authorization_code', 'refresh_token'], allowed_scopes: ['openid', 'profile'], token_ttl_seconds: 3600, id_token_ttl_seconds: 3600, access_token_ttl_seconds: 3600, refresh_token_ttl_seconds: 86400, require_pkce: false, token_endpoint_auth_method: 'client_secret_basic', });}
function createRefreshToken(db: DatabaseService, token: string, clientId = 'test-client') { const tokenHash = crypto.createHash('sha256').update(token).digest('hex'); db.saveRefreshToken({ token_hash: tokenHash, client_id: clientId, did: 'did:plc:testuser', handle: 'test.bsky.social', scope: 'openid profile', expires_at: new Date(Date.now() + 86400 * 1000), family_id: `family-${Date.now()}`, revoked: false, }); return tokenHash;}
describe('POST /oauth/revoke', () => { let db: DatabaseService; let app: express.Application;
beforeEach(() => { db = new DatabaseService(':memory:'); app = createTestApp(db); registerOIDCClient(db); });
afterEach(() => db.close());
it('should return 200 for missing token (per RFC 7009)', async () => { const res = await request(app) .post('/oauth/revoke') .send({});
expect(res.status).toBe(200); });
it('should revoke a refresh token', async () => { const rawToken = 'test-refresh-token-123'; const tokenHash = createRefreshToken(db, rawToken);
const res = await request(app) .post('/oauth/revoke') .send({ token: rawToken, client_id: 'test-client', client_secret: 'my-secret' });
expect(res.status).toBe(200);
// Token should be revoked in DB const stored = db.getRefreshToken(tokenHash); expect(stored?.revoked).toBe(true); });
it('should accept client credentials via Basic auth', async () => { const rawToken = 'test-refresh-token-basic'; createRefreshToken(db, rawToken);
const credentials = Buffer.from('test-client:my-secret').toString('base64'); const res = await request(app) .post('/oauth/revoke') .set('Authorization', `Basic ${credentials}`) .send({ token: rawToken });
expect(res.status).toBe(200); });
it('should return 401 for unknown client', async () => { const res = await request(app) .post('/oauth/revoke') .send({ token: 'some-token', client_id: 'nonexistent' });
expect(res.status).toBe(401); expect(res.body.error).toBe('invalid_client'); });
it('should return 401 for wrong client secret', async () => { const res = await request(app) .post('/oauth/revoke') .send({ token: 'some-token', client_id: 'test-client', client_secret: 'wrong-secret' });
expect(res.status).toBe(401); expect(res.body.error).toBe('invalid_client'); });
it('should return 401 for missing client secret when required', async () => { const res = await request(app) .post('/oauth/revoke') .send({ token: 'some-token', client_id: 'test-client' });
expect(res.status).toBe(401); expect(res.body.error).toBe('invalid_client'); expect(res.body.error_description).toContain('Client authentication required'); });
it('should return 200 for unknown token (per RFC 7009)', async () => { const res = await request(app) .post('/oauth/revoke') .send({ token: 'nonexistent-token', client_id: 'test-client', client_secret: 'my-secret' });
expect(res.status).toBe(200); });
it('should return 200 for token belonging to different client', async () => { // Register a second client so FK constraint is satisfied registerOIDCClient(db, 'other-client', 'other-secret');
const rawToken = 'other-client-token'; createRefreshToken(db, rawToken, 'other-client');
const res = await request(app) .post('/oauth/revoke') .send({ token: rawToken, client_id: 'test-client', client_secret: 'my-secret' });
// Per RFC 7009, still returns 200 expect(res.status).toBe(200); });
it('should handle access token revocation (returns 200)', async () => { const res = await request(app) .post('/oauth/revoke') .send({ token: 'some-access-token', token_type_hint: 'access_token', client_id: 'test-client', client_secret: 'my-secret', });
// Access tokens can't be truly revoked (JWT), returns 200 per RFC expect(res.status).toBe(200); });});