31d514b7ca
Nur bei gueltiger Signatur und ohne ausstehenden Kennwortwechsel; next ueber sanitizeNextPath, /login als Ziel -> Dashboard. Gesperrte Konten: API lehnt ab, Oberflaeche loescht das Cookie serverseitig, keine Schleife. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
190 lines
7.7 KiB
TypeScript
190 lines
7.7 KiB
TypeScript
// @vitest-environment node
|
|
|
|
import { SignJWT } from 'jose';
|
|
import { NextRequest } from 'next/server';
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
import { middleware } from './middleware';
|
|
|
|
/**
|
|
* middleware.test — Desktop-Client-Cookie (260917-h2s), eigener describe-Block
|
|
* neben den bestehenden Redirect-/Session-Faellen. `@vitest-environment node`,
|
|
* weil `NextRequest`/`NextResponse` node-typische APIs (Headers, URL) nutzen,
|
|
* die im jsdom-Standardmilieu der Suite nicht gebraucht werden.
|
|
*/
|
|
describe('middleware — Desktop-Client-Cookie (260917-h2s)', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('JWT_SECRET', 'test-secret');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
it('Test 1: /login?desktop=1 setzt das Cookie tessera_desktop=1', async () => {
|
|
const req = new NextRequest('http://localhost:3000/login?desktop=1');
|
|
const res = await middleware(req);
|
|
const setCookie = res.headers.get('set-cookie');
|
|
expect(setCookie).toContain('tessera_desktop=1');
|
|
expect(setCookie).toContain('Path=/');
|
|
expect(setCookie).toContain('Max-Age=31536000');
|
|
expect(setCookie).toContain('SameSite=lax');
|
|
expect(setCookie).not.toContain('Secure');
|
|
expect(setCookie).not.toContain('HttpOnly');
|
|
});
|
|
|
|
it('Test 2: /login ohne Parameter setzt kein Cookie', async () => {
|
|
const req = new NextRequest('http://localhost:3000/login');
|
|
const res = await middleware(req);
|
|
expect(res.headers.get('set-cookie')).toBeNull();
|
|
});
|
|
|
|
it('Test 3: /dashboard?desktop=1 ohne Session leitet um und setzt das Cookie', async () => {
|
|
const req = new NextRequest('http://localhost:3000/dashboard?desktop=1');
|
|
const res = await middleware(req);
|
|
expect(res.status).toBe(307);
|
|
expect(res.headers.get('location')).toContain('/login');
|
|
expect(res.headers.get('set-cookie')).toContain('tessera_desktop=1');
|
|
});
|
|
|
|
it('Test 4: https setzt Secure', async () => {
|
|
const req = new NextRequest('https://tessera.example.com/login?desktop=1');
|
|
const res = await middleware(req);
|
|
expect(res.headers.get('set-cookie')).toContain('Secure');
|
|
});
|
|
|
|
it('Test 5: gueltiges JWT laesst die Anfrage durch und setzt trotzdem das Cookie', async () => {
|
|
const token = await new SignJWT({ sub: 'u1' })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('5m')
|
|
.sign(new TextEncoder().encode('test-secret'));
|
|
|
|
const req = new NextRequest('http://localhost:3000/dashboard?desktop=1', {
|
|
headers: { cookie: `session=${token}` },
|
|
});
|
|
const res = await middleware(req);
|
|
expect(res.headers.get('set-cookie')).toContain('tessera_desktop=1');
|
|
expect(res.headers.get('x-middleware-next')).toBe('1');
|
|
});
|
|
|
|
it('Test 6 (quick-260918-gza): /login mit dv/dc/dos setzt zusaetzlich tessera_desktop_client', async () => {
|
|
const req = new NextRequest(
|
|
'http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=windows',
|
|
);
|
|
const res = await middleware(req);
|
|
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
|
|
expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|windows');
|
|
|
|
const setCookie = res.headers.get('set-cookie');
|
|
expect(setCookie).toContain('tessera_desktop_client=1.2.0%7Ca6d1a64%7Cwindows');
|
|
expect(setCookie).toContain('Max-Age=31536000');
|
|
expect(setCookie).toContain('Path=/');
|
|
// Kein HttpOnly fuer DIESES Cookie -- der bestehende tessera_desktop
|
|
// liefert ebenfalls kein HttpOnly, darum genuegt die Wertpruefung oben.
|
|
});
|
|
|
|
it('Test 7 (quick-260918-gza, alter Client): /login?desktop=1 ohne dv/dc/dos setzt kein tessera_desktop_client', async () => {
|
|
const req = new NextRequest('http://localhost:3000/login?desktop=1');
|
|
const res = await middleware(req);
|
|
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
|
|
expect(res.cookies.get('tessera_desktop_client')).toBeUndefined();
|
|
});
|
|
|
|
it('Test 8 (quick-260918-gza, Bereinigung): ungueltige oder fehlende Werte setzen kein tessera_desktop_client', async () => {
|
|
const angleBrackets = new NextRequest(
|
|
'http://localhost:3000/login?desktop=1&dv=1.2.0%3Cscript%3E&dc=a6d1a64&dos=windows',
|
|
);
|
|
expect((await middleware(angleBrackets)).cookies.get('tessera_desktop_client')).toBeUndefined();
|
|
|
|
const spaceInOs = new NextRequest(
|
|
'http://localhost:3000/login?desktop=1&dv=1.2.0&dc=a6d1a64&dos=win%20dows',
|
|
);
|
|
expect((await middleware(spaceInOs)).cookies.get('tessera_desktop_client')).toBeUndefined();
|
|
|
|
const missingDv = new NextRequest(
|
|
'http://localhost:3000/login?desktop=1&dc=a6d1a64&dos=windows',
|
|
);
|
|
expect((await middleware(missingDv)).cookies.get('tessera_desktop_client')).toBeUndefined();
|
|
|
|
const emptyCommit = new NextRequest(
|
|
'http://localhost:3000/login?desktop=1&dv=1.2.0&dc=&dos=linux',
|
|
);
|
|
expect((await middleware(emptyCommit)).cookies.get('tessera_desktop_client')?.value).toBe(
|
|
'1.2.0||linux',
|
|
);
|
|
});
|
|
|
|
it('Test 9 (quick-260918-gza, Redirect-Pfad): /dashboard ohne Session setzt beide Cookies auf dem 307', async () => {
|
|
const req = new NextRequest(
|
|
'http://localhost:3000/dashboard?desktop=1&dv=1.2.0&dc=a6d1a64&dos=linux',
|
|
);
|
|
const res = await middleware(req);
|
|
expect(res.status).toBe(307);
|
|
expect(res.headers.get('location')).toContain('/login');
|
|
expect(res.cookies.get('tessera_desktop')?.value).toBe('1');
|
|
expect(res.cookies.get('tessera_desktop_client')?.value).toBe('1.2.0|a6d1a64|linux');
|
|
});
|
|
});
|
|
|
|
describe('middleware — /login bei bestehender Anmeldung (quick-260930)', () => {
|
|
beforeEach(() => {
|
|
vi.stubEnv('JWT_SECRET', 'test-secret');
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllEnvs();
|
|
});
|
|
|
|
async function token(claims: Record<string, unknown> = {}) {
|
|
return new SignJWT({ sub: 'u1', ...claims })
|
|
.setProtectedHeader({ alg: 'HS256' })
|
|
.setIssuedAt()
|
|
.setExpirationTime('5m')
|
|
.sign(new TextEncoder().encode('test-secret'));
|
|
}
|
|
|
|
function loginReq(url: string, session?: string) {
|
|
return new NextRequest(url, session ? { headers: { cookie: `session=${session}` } } : {});
|
|
}
|
|
|
|
it('angemeldet: /login leitet aufs Dashboard um', async () => {
|
|
const res = await middleware(loginReq('http://localhost:3000/login', await token()));
|
|
expect(res.status).toBe(307);
|
|
expect(new URL(res.headers.get('location') as string).pathname).toBe('/');
|
|
});
|
|
|
|
it('angemeldet mit sicherem next: dorthin', async () => {
|
|
const res = await middleware(
|
|
loginReq('http://localhost:3000/login?next=%2Fadmin%2Fusers', await token()),
|
|
);
|
|
expect(new URL(res.headers.get('location') as string).pathname).toBe('/admin/users');
|
|
});
|
|
|
|
it('angemeldet mit fremdem oder zirkulaerem next: Dashboard', async () => {
|
|
for (const next of ['https%3A%2F%2Fboese.example', '%2F%2Fboese.example', '%2Flogin']) {
|
|
const res = await middleware(
|
|
loginReq(`http://localhost:3000/login?next=${next}`, await token()),
|
|
);
|
|
const loc = new URL(res.headers.get('location') as string);
|
|
expect(loc.host).toBe('localhost:3000');
|
|
expect(loc.pathname).toBe('/');
|
|
}
|
|
});
|
|
|
|
it('ohne oder mit ungueltigem Cookie: Anmeldeseite wie bisher', async () => {
|
|
const ohne = await middleware(loginReq('http://localhost:3000/login'));
|
|
expect(ohne.headers.get('x-middleware-next')).toBe('1');
|
|
const kaputt = await middleware(
|
|
loginReq('http://localhost:3000/login', 'kein.gueltiges.token'),
|
|
);
|
|
expect(kaputt.headers.get('x-middleware-next')).toBe('1');
|
|
});
|
|
|
|
it('Kennwortwechsel ausstehend: keine Umleitung von /login', async () => {
|
|
const res = await middleware(
|
|
loginReq('http://localhost:3000/login', await token({ mustChangePassword: true })),
|
|
);
|
|
expect(res.headers.get('x-middleware-next')).toBe('1');
|
|
});
|
|
});
|