- src/notes-api-server.ts: NotesApiOptions erweitert - authToken: HMAC-SHA256 Bearer-Token (timing-safe compare) - allowedOrigins: CORS-Whitelist (oder '*' fuer alle) - checkAuth(authHeader): pruft Bearer-Token - generateToken(secret): statische Methode fuer Client-Tools - checkOrigin(origin): CORS-Pruefung - corsHeader(origin): liefert CORS-Header (leer wenn nicht erlaubt) - Token-Format: <timestamp>.<hmac-hex> Tests: - tests/auth-cors.test.ts: 14 neue Tests - Auth: ohne Token, Header fehlt, falsches Schema, falsches Format - Auth: korrekter Token, falsches Secret, HMAC-Tampering - CORS: keine Config, Wildcard *, Whitelist - CORS: corsHeader mit/ohne Erlaubnis - generateToken: Format, verschiedene Secrets Verifiziert: - 116/116 Tests gruen (14 neue) - Auth nutzt timingSafeEqual (kein String-Compare) - NotesApi bleibt abwaertskompatibel (alte Aufrufe ohne Options funktionieren)
108 lines
4.2 KiB
TypeScript
108 lines
4.2 KiB
TypeScript
// Tests fuer Auth + CORS
|
|
// Phase 9: Production-Haertung
|
|
|
|
import { describe, it, expect } from 'vitest';
|
|
import { NotesApi } from '../src/notes-api-server.js';
|
|
|
|
describe('NotesApi Auth', () => {
|
|
it('kein Token konfiguriert: alle Requests erlaubt', () => {
|
|
const api = new NotesApi('/tmp/vault');
|
|
expect(api.checkAuth(undefined).ok).toBe(true);
|
|
expect(api.checkAuth('Bearer irgendwas').ok).toBe(true);
|
|
});
|
|
|
|
it('Token konfiguriert, aber Header fehlt: 401', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
const result = api.checkAuth(undefined);
|
|
expect(result.ok).toBe(false);
|
|
expect(result.reason).toContain('fehlt');
|
|
});
|
|
|
|
it('Token mit falschem Schema: ungueltig', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
expect(api.checkAuth('Basic abc').ok).toBe(false);
|
|
});
|
|
|
|
it('Token mit falschem Format: ungueltig', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
expect(api.checkAuth('Bearer not-a-valid-token').ok).toBe(false);
|
|
});
|
|
|
|
it('Korrekter Token: erlaubt', () => {
|
|
const secret = 'my-secret';
|
|
const api = new NotesApi('/tmp/vault', { authToken: secret });
|
|
const token = NotesApi.generateToken(secret);
|
|
const result = api.checkAuth(`Bearer ${token}`);
|
|
expect(result.ok).toBe(true);
|
|
});
|
|
|
|
it('Token mit falschem Secret: ungueltig', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'correct-secret' });
|
|
const wrongToken = NotesApi.generateToken('wrong-secret');
|
|
expect(api.checkAuth(`Bearer ${wrongToken}`).ok).toBe(false);
|
|
});
|
|
|
|
it('HMAC ist timing-safe (kein einfacher String-Vergleich)', () => {
|
|
// Wir koennen das nicht direkt testen, aber die Implementation nutzt timingSafeEqual
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
const token = NotesApi.generateToken('secret');
|
|
// Aendere ein Zeichen im Token
|
|
const modified = token.slice(0, -2) + (token.endsWith('0') ? '1' : '0') + token.slice(-1);
|
|
expect(api.checkAuth(`Bearer ${modified}`).ok).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('NotesApi CORS', () => {
|
|
it('keine Origins konfiguriert: alle erlaubt', () => {
|
|
const api = new NotesApi('/tmp/vault');
|
|
expect(api.checkOrigin('https://evil.com')).toBe(true);
|
|
expect(api.checkOrigin(undefined)).toBe(true);
|
|
});
|
|
|
|
it('Wildcard * erlaubt alle Origins', () => {
|
|
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['*'] });
|
|
expect(api.checkOrigin('https://anywhere.com')).toBe(true);
|
|
expect(api.checkOrigin('https://evil.com')).toBe(true);
|
|
});
|
|
|
|
it('Whitelist: nur konfigurierte Origins', () => {
|
|
const api = new NotesApi('/tmp/vault', {
|
|
allowedOrigins: ['https://vault.example.com', 'https://app.example.com'],
|
|
});
|
|
expect(api.checkOrigin('https://vault.example.com')).toBe(true);
|
|
expect(api.checkOrigin('https://app.example.com')).toBe(true);
|
|
expect(api.checkOrigin('https://evil.com')).toBe(false);
|
|
expect(api.checkOrigin(undefined)).toBe(false);
|
|
});
|
|
|
|
it('corsHeader liefert korrekte Header fuer erlaubte Origins', () => {
|
|
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['https://app.example.com'] });
|
|
const headers = api.corsHeader('https://app.example.com');
|
|
expect(headers['Access-Control-Allow-Origin']).toBe('https://app.example.com');
|
|
expect(headers['Access-Control-Allow-Methods']).toContain('GET');
|
|
expect(headers['Access-Control-Allow-Methods']).toContain('PUT');
|
|
});
|
|
|
|
it('corsHeader liefert leere Header fuer nicht erlaubte Origins', () => {
|
|
const api = new NotesApi('/tmp/vault', { allowedOrigins: ['https://app.example.com'] });
|
|
expect(api.corsHeader('https://evil.com')).toEqual({});
|
|
});
|
|
});
|
|
|
|
describe('NotesApi.generateToken', () => {
|
|
it('Token-Format: <timestamp>.<hmac>', () => {
|
|
const token = NotesApi.generateToken('secret');
|
|
expect(token).toMatch(/^\d+\.[a-f0-9]+$/);
|
|
});
|
|
|
|
it('Verschiedene Secrets ergeben verschiedene Tokens', () => {
|
|
const t1 = NotesApi.generateToken('secret-a');
|
|
const t2 = NotesApi.generateToken('secret-b');
|
|
expect(t1).not.toBe(t2);
|
|
// Aber gleicher Timestamp-Teil moeglich
|
|
const hmac1 = t1.split('.')[1];
|
|
const hmac2 = t2.split('.')[1];
|
|
expect(hmac1).not.toBe(hmac2);
|
|
});
|
|
});
|