chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
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,
);
});
});