Initial Commit
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { generateKeyPair, exportPKCS8, jwtVerify } from 'jose';
|
||||
import { TokenCipher, randomSecret, hashSecret, pkceChallenge, applicationJwt, assertSameOrigin, assertOAuthSessionBinding } from '../src/lib/server/security';
|
||||
|
||||
describe('server credential boundaries', () => {
|
||||
test('authenticated encryption binds purpose and detects tampering', async () => {
|
||||
const cipher = new TokenCipher(randomBytes(32).toString('base64'));
|
||||
const envelope = await cipher.seal({ refresh: 'never-in-browser' }, 'character:123');
|
||||
expect(envelope).not.toContain('never-in-browser');
|
||||
expect(await cipher.open<{ refresh: string }>(envelope, 'character:123')).toEqual({ refresh: 'never-in-browser' });
|
||||
await expect(cipher.open(envelope, 'character:456')).rejects.toThrow();
|
||||
const parts = envelope.split('.');
|
||||
parts[2] = (parts[2]![0] === 'A' ? 'B' : 'A') + parts[2]!.slice(1);
|
||||
await expect(cipher.open(parts.join('.'), 'character:123')).rejects.toThrow();
|
||||
expect(() => new TokenCipher('short')).toThrow();
|
||||
});
|
||||
test('opaque secrets and PKCE S256 vector', () => {
|
||||
expect(randomSecret()).not.toBe(randomSecret());
|
||||
expect(hashSecret(randomSecret())).toHaveLength(64);
|
||||
expect(pkceChallenge('dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk')).toBe('E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM');
|
||||
});
|
||||
test('application token audience is distinct from EVE and is short-lived', async () => {
|
||||
const { privateKey, publicKey } = await generateKeyPair('RS256', { extractable: true });
|
||||
const token = await applicationJwt({ userId: 'u1', sessionId: 's1', issuer: 'https://app.example', privateKey: Buffer.from(await exportPKCS8(privateKey)).toString('base64') });
|
||||
const { payload } = await jwtVerify(token, publicKey, { issuer: 'https://app.example', audience: 'eve-intelligence' });
|
||||
expect(payload.sub).toBe('u1'); expect(payload.sid).toBe('s1');
|
||||
expect(payload.exp! - payload.iat!).toBe(300);
|
||||
await expect(jwtVerify(token, publicKey, { audience: 'EVE Online' })).rejects.toThrow();
|
||||
});
|
||||
test('OAuth linking cannot switch user or browser session during the round-trip', () => {
|
||||
const cookie = randomSecret();
|
||||
const binding = { expectedUserId: 'user1', expectedSessionHash: hashSecret(cookie), currentUserId: 'user1', currentCookie: cookie };
|
||||
assertOAuthSessionBinding(binding);
|
||||
expect(() => assertOAuthSessionBinding({ ...binding, currentUserId: 'user2' })).toThrow();
|
||||
expect(() => assertOAuthSessionBinding({ ...binding, currentCookie: randomSecret() })).toThrow();
|
||||
expect(() => assertOAuthSessionBinding({ ...binding, currentUserId: null })).toThrow();
|
||||
expect(() => assertOAuthSessionBinding({ expectedSessionHash: null, currentUserId: 'unexpected-login' })).toThrow();
|
||||
assertOAuthSessionBinding({ expectedSessionHash: null, currentUserId: null });
|
||||
});
|
||||
test('state-changing browser actions reject missing/foreign origins', () => {
|
||||
expect(() => assertSameOrigin(new Request('https://app.example/api', { method: 'POST' }), 'https://app.example')).toThrow();
|
||||
expect(() => assertSameOrigin(new Request('https://app.example/api', { headers: { origin: 'https://evil.example' } }), 'https://app.example')).toThrow();
|
||||
assertSameOrigin(new Request('https://app.example/api', { headers: { origin: 'https://app.example' } }), 'https://app.example');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user