import { createAccessJwt, createRefreshJwt, verifyAccessJwt, verifyRefreshJwt, } from '@pdsjs/core/auth'; import { base64UrlDecode } from '@pdsjs/core/crypto'; import { describe, expect, test } from 'vitest'; describe('JWT Creation', () => { test('createAccessJwt creates valid JWT structure', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); const parts = jwt.split('.'); expect(parts.length).toBe(3); // Decode header const header = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[0])), ); expect(header.typ).toBe('at+jwt'); expect(header.alg).toBe('HS256'); // Decode payload const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); expect(payload.scope).toBe('com.atproto.access'); expect(payload.sub).toBe(did); expect(payload.aud).toBe(did); expect(payload.iat > 0).toBe(true); expect(payload.exp > payload.iat).toBe(true); }); test('createRefreshJwt creates valid JWT with jti', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); const parts = jwt.split('.'); const header = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[0])), ); expect(header.typ).toBe('refresh+jwt'); const payload = JSON.parse( new TextDecoder().decode(base64UrlDecode(parts[1])), ); expect(payload.scope).toBe('com.atproto.refresh'); expect(payload.jti).toBeTruthy(); // has unique token ID }); }); describe('JWT Verification', () => { test('verifyAccessJwt returns payload for valid token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); const payload = await verifyAccessJwt(jwt, secret); expect(payload.sub).toBe(did); expect(payload.scope).toBe('com.atproto.access'); }); test('verifyAccessJwt throws for wrong secret', async () => { const did = 'did:web:test.example'; const jwt = await createAccessJwt(did, 'correct-secret'); await expect(() => verifyAccessJwt(jwt, 'wrong-secret')).rejects.toThrow( /invalid signature/i, ); }); test('verifyAccessJwt throws for expired token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; // Create token that expired 1 second ago const jwt = await createAccessJwt(did, secret, -1); await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow( /expired/i, ); }); test('verifyAccessJwt throws for refresh token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); await expect(() => verifyAccessJwt(jwt, secret)).rejects.toThrow( /invalid token type/i, ); }); test('verifyRefreshJwt returns payload for valid token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createRefreshJwt(did, secret); const payload = await verifyRefreshJwt(jwt, secret); expect(payload.sub).toBe(did); expect(payload.scope).toBe('com.atproto.refresh'); expect(payload.jti).toBeTruthy(); // has token ID }); test('verifyRefreshJwt throws for wrong secret', async () => { const did = 'did:web:test.example'; const jwt = await createRefreshJwt(did, 'correct-secret'); await expect(() => verifyRefreshJwt(jwt, 'wrong-secret')).rejects.toThrow( /invalid signature/i, ); }); test('verifyRefreshJwt throws for expired token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; // Create token that expired 1 second ago const jwt = await createRefreshJwt(did, secret, -1); await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow( /expired/i, ); }); test('verifyRefreshJwt throws for access token', async () => { const did = 'did:web:test.example'; const secret = 'test-secret-key'; const jwt = await createAccessJwt(did, secret); await expect(() => verifyRefreshJwt(jwt, secret)).rejects.toThrow( /invalid token type/i, ); }); test('verifyAccessJwt throws for malformed JWT', async () => { const secret = 'test-secret-key'; // Not a JWT at all await expect(() => verifyAccessJwt('not-a-jwt', secret)).rejects.toThrow( /Invalid JWT format/i, ); // Only two parts await expect(() => verifyAccessJwt('two.parts', secret)).rejects.toThrow( /Invalid JWT format/i, ); // Four parts await expect(() => verifyAccessJwt('one.two.three.four', secret), ).rejects.toThrow(/Invalid JWT format/i); }); test('verifyRefreshJwt throws for malformed JWT', async () => { const secret = 'test-secret-key'; await expect(() => verifyRefreshJwt('not-a-jwt', secret)).rejects.toThrow( /Invalid JWT format/i, ); await expect(() => verifyRefreshJwt('two.parts', secret)).rejects.toThrow( /Invalid JWT format/i, ); }); });