- Alter Test 'HMAC ist timing-safe' war fehlerhaft:
- aenderte vorletztes Zeichen via endsWith('0')-Logik
- in Edge-Cases konnte modified === token sein
- Zwei neue, deterministische Tests:
1. 'HMAC-Tampering wird erkannt': aendert garantiert das ERSTE Zeichen
des HMAC, verifiziert modified !== token UND checkAuth lehnt ab
2. 'Token mit geaendertem Timestamp aber gleichem HMAC wird abgelehnt':
verifiziert, dass HMAC tatsaechlich an ts gebunden ist (nicht statisch)
Verifiziert:
- 117/117 Tests gruen (vorher 116, 1 Test in 2 aufgeteilt)
- HMAC-Implementation in src/notes-api-server.ts ist korrekt
(HMAC wird ueber ts berechnet, daher kann ein anderer ts mit gleichem hmac nicht passieren)
122 lines
4.8 KiB
TypeScript
122 lines
4.8 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-Tampering wird erkannt (deterministisch anderes Token)', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
const token = NotesApi.generateToken('secret');
|
|
const [ts, hmac] = token.split('.');
|
|
// Aendere das ERSTE Zeichen des HMAC garantiert
|
|
const firstChar = hmac[0];
|
|
const replacement = firstChar === '0' ? '1' : '0';
|
|
const modifiedHmac = replacement + hmac.slice(1);
|
|
const modified = `${ts}.${modifiedHmac}`;
|
|
expect(modified).not.toBe(token);
|
|
expect(api.checkAuth(`Bearer ${modified}`).ok).toBe(false);
|
|
});
|
|
|
|
it('Token mit geaendertem Timestamp aber gleichem HMAC wird abgelehnt', () => {
|
|
const api = new NotesApi('/tmp/vault', { authToken: 'secret' });
|
|
const token = NotesApi.generateToken('secret');
|
|
const [, hmac] = token.split('.');
|
|
// Ersetze Timestamp durch anderen Wert, behalte HMAC -> muss abgelehnt werden
|
|
// (HMAC haengt am Timestamp, also passt der alte HMAC nicht mehr)
|
|
const fakeToken = `9999999999999.${hmac}`;
|
|
expect(api.checkAuth(`Bearer ${fakeToken}`).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);
|
|
});
|
|
});
|