Wiki/bridge/tests/middleware/auth.test.ts
Corentin JOGUET a79c51e6f2
Some checks are pending
CI / Type-check bridge (push) Blocked by required conditions
CI / Tests unit bridge (push) Blocked by required conditions
CI / Lint bridge (Biome) (push) Waiting to run
CI / Tests integration bridge (push) Blocked by required conditions
CI / Security scan (push) Waiting to run
CI / Docker build + healthcheck (push) Blocked by required conditions
feat(auth): R2.3b bridge accepte JWT HMAC DocAdenice via DOCMOST_APP_SECRET
2026-05-07 23:02:01 +02:00

726 lines
25 KiB
TypeScript

/**
* Tests integration auth middleware (R1 generique) — dual mode service-token + OIDC.
*
* R1 — Plus de lookup PersonneRepo, plus de roles formation-hub. Le claim
* `acadenice_permissions[]` du JWT alimente directement les scopes.
*
* Strategie JWKS : mini serveur HTTP local qui expose un /.well-known/jwks.json
* avec une cle RSA generee a la volee via `jose.generateKeyPair`. Plus realiste
* que mocker `createRemoteJWKSet` car teste fetch reel + parsing reel.
*/
import { randomBytes } from 'node:crypto';
import { type Server, createServer } from 'node:http';
import type { AddressInfo } from 'node:net';
import { Hono } from 'hono';
import { type CryptoKey, type JWK, SignJWT, exportJWK, generateKeyPair } from 'jose';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { logger } from '../../src/lib/logger.js';
import {
type ApiTokenRecord,
type AuthVariables,
authMiddleware,
extractPermissions,
hasScope,
parseTokens,
requireScope,
} from '../../src/middleware/auth.js';
import { DocmostJwtVerifier } from '../../src/middleware/docmost-jwt-verifier.js';
import { errorHandler } from '../../src/middleware/error-handler.js';
import { OidcVerifier } from '../../src/middleware/oidc-verifier.js';
// ---------------------------------------------------------------------------
// JWKS mini server
// ---------------------------------------------------------------------------
interface JwksFixture {
server: Server;
url: string;
privateKey: CryptoKey;
publicJwk: JWK;
kid: string;
hits: number;
}
async function startJwksServer(): Promise<JwksFixture> {
const { privateKey, publicKey } = await generateKeyPair('RS256');
const publicJwk = await exportJWK(publicKey);
const kid = 'test-key-1';
publicJwk.kid = kid;
publicJwk.alg = 'RS256';
publicJwk.use = 'sig';
const fixture: JwksFixture = {
server: createServer(),
url: '',
privateKey,
publicJwk,
kid,
hits: 0,
};
fixture.server.on('request', (req, res) => {
if (req.url === '/jwks.json') {
fixture.hits += 1;
res.writeHead(200, { 'content-type': 'application/json' });
res.end(JSON.stringify({ keys: [publicJwk] }));
return;
}
res.writeHead(404);
res.end();
});
await new Promise<void>((resolve) => fixture.server.listen(0, '127.0.0.1', resolve));
const addr = fixture.server.address() as AddressInfo;
fixture.url = `http://127.0.0.1:${addr.port}/jwks.json`;
return fixture;
}
async function signJwt(
fx: JwksFixture,
claims: Record<string, unknown>,
overrides?: { issuer?: string; audience?: string; expiresIn?: string; alg?: string },
): Promise<string> {
const builder = new SignJWT(claims)
.setProtectedHeader({ alg: overrides?.alg ?? 'RS256', kid: fx.kid })
.setIssuedAt()
.setIssuer(overrides?.issuer ?? 'https://auth.test/issuer')
.setAudience(overrides?.audience ?? 'formation-hub-bridge')
.setExpirationTime(overrides?.expiresIn ?? '5m');
return builder.sign(fx.privateKey);
}
// ---------------------------------------------------------------------------
// App builder
// ---------------------------------------------------------------------------
interface BuildAppOpts {
tokens?: ApiTokenRecord[];
oidcEnabled: boolean;
jwks?: JwksFixture;
groupsScopesMap?: Record<string, string[]>;
docmostSecret?: string;
docmostIssuer?: string;
docmostAudience?: string;
}
function buildApp(opts: BuildAppOpts) {
const tokens = opts.tokens ?? [];
const map = new Map<string, ApiTokenRecord>();
for (const t of tokens) map.set(t.token, t);
let verifier: OidcVerifier | null = null;
if (opts.oidcEnabled) {
if (!opts.jwks) throw new Error('jwks fixture required when oidcEnabled');
verifier = new OidcVerifier({
issuer: 'https://auth.test/issuer',
jwksUri: opts.jwks.url,
audience: 'formation-hub-bridge',
logger,
jwksCacheMaxAgeMs: 1000,
});
}
let docmostJwt: DocmostJwtVerifier | null = null;
if (opts.docmostSecret) {
docmostJwt = new DocmostJwtVerifier({
secret: opts.docmostSecret,
issuer: opts.docmostIssuer ?? 'Docmost',
audience: opts.docmostAudience,
logger,
});
}
const app = new Hono<{ Variables: AuthVariables }>();
app.onError(errorHandler);
app.use(
'/protected/*',
authMiddleware({
tokens: map,
oidc: verifier,
docmostJwt,
groupsScopesMap: opts.groupsScopesMap ?? {},
logger,
}),
);
app.get('/protected/me', (c) => {
const user = c.get('user');
return c.json({ user });
});
app.get('/protected/needs-read-tables', requireScope('read:tables'), (c) =>
c.json({ ok: true, scopes: c.get('user').scopes }),
);
app.get('/protected/needs-admin', requireScope('admin:write'), (c) => c.json({ ok: true }));
return { app };
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
describe('parseTokens', () => {
it('parse JSON valide', () => {
const map = parseTokens(
JSON.stringify([{ token: 'brg_x', name: 'a', scopes: ['read:tables'] }]),
);
expect(map.get('brg_x')?.name).toBe('a');
});
it('retourne map vide si raw vide', () => {
expect(parseTokens(undefined).size).toBe(0);
expect(parseTokens('').size).toBe(0);
});
it('throw si JSON invalide', () => {
expect(() => parseTokens('{nope')).toThrow(/JSON/);
});
it('throw si pas un array', () => {
expect(() => parseTokens('{"foo": 1}')).toThrow(/tableau/);
});
it('throw si entree manque token/name/scopes', () => {
expect(() => parseTokens('[{"token":"x"}]')).toThrow();
expect(() => parseTokens('[{"token":"x","name":"y"}]')).toThrow();
expect(() => parseTokens('[{"token":"x","name":"y","scopes":[1]}]')).toThrow();
});
});
describe('hasScope', () => {
it('match exact', () => {
expect(hasScope(new Set(['read:tables']), 'read:tables')).toBe(true);
expect(hasScope(new Set(['read:tables']), 'write:tables')).toBe(false);
});
it('admin:* couvre tout', () => {
expect(hasScope(new Set(['admin:*']), 'read:any')).toBe(true);
});
it('prefix wildcard couvre meme prefix', () => {
expect(hasScope(new Set(['read:*']), 'read:tables')).toBe(true);
expect(hasScope(new Set(['read:*']), 'write:tables')).toBe(false);
});
});
describe('extractPermissions', () => {
it('retourne le claim acadenice_permissions[] si present', () => {
expect(extractPermissions({ acadenice_permissions: ['a', 'b'] })).toEqual(['a', 'b']);
});
it('retourne [] si claim absent', () => {
expect(extractPermissions({})).toEqual([]);
});
it('retourne [] si claim pas un array', () => {
expect(extractPermissions({ acadenice_permissions: 'foo' })).toEqual([]);
});
it('filtre les valeurs non-strings et vides', () => {
expect(extractPermissions({ acadenice_permissions: ['ok', 1, '', null, 'good'] })).toEqual([
'ok',
'good',
]);
});
});
describe('auth middleware — service tokens', () => {
const tokens: ApiTokenRecord[] = [
{ token: 'brg_valid', name: 'demo', scopes: ['read:tables', 'admin:write'] },
];
it('cas 1 — service token valid via Bearer -> 200 + source=service-token', async () => {
const { app } = buildApp({ tokens, oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Authorization: 'Bearer brg_valid' },
});
expect(res.status).toBe(200);
const body = (await res.json()) as {
user: { source: string; scopes: string[]; tokenId: string };
};
expect(body.user.source).toBe('service-token');
expect(body.user.tokenId).toBe('demo');
expect(body.user.scopes).toContain('read:tables');
});
it('service token valid via ApiKey scheme -> 200', async () => {
const { app } = buildApp({ tokens, oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Authorization: 'ApiKey brg_valid' },
});
expect(res.status).toBe(200);
});
it('cas 2 — service token invalid -> 401 AUTH_INVALID', async () => {
const { app } = buildApp({ tokens, oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Authorization: 'Bearer brg_unknown' },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('header sans scheme reconnu -> 401 AUTH_INVALID', async () => {
const { app } = buildApp({ tokens, oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Authorization: 'Token brg_valid' },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('aucun header -> 401 AUTH_REQUIRED', async () => {
const { app } = buildApp({ tokens, oidcEnabled: false });
const res = await app.request('/protected/me');
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_REQUIRED');
});
});
describe('auth middleware — OIDC desactive + JWT envoye', () => {
it('cas 3 — OIDC off + Bearer non-brg -> 401 (pas de fallback silencieux)', async () => {
const { app } = buildApp({ oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Authorization: 'Bearer eyJhbGciOiJSUzI1NiJ9.fake.fake' },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('OIDC off + cookie authToken -> 401', async () => {
const { app } = buildApp({ oidcEnabled: false });
const res = await app.request('/protected/me', {
headers: { Cookie: 'authToken=eyJhbGciOiJSUzI1NiJ9.fake.fake' },
});
expect(res.status).toBe(401);
});
});
describe('auth middleware — OIDC actif (R1 generique)', () => {
let jwks: JwksFixture;
beforeAll(async () => {
jwks = await startJwksServer();
});
afterAll(async () => {
await new Promise<void>((resolve) => jwks.server.close(() => resolve()));
});
it('cas 4 — JWT valid + claim acadenice_permissions[] -> 200, scopes alimentes directement', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, {
email: 'jane@acadenice.fr',
sub: 'authentik-jane-uuid',
groups: [],
acadenice_permissions: ['read:tables', 'write:tables'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as {
user: {
source: string;
scopes: string[];
permissions: string[];
sub: string;
email: string;
};
};
expect(body.user.source).toBe('oidc-jwt');
expect(body.user.email).toBe('jane@acadenice.fr');
expect(body.user.sub).toBe('authentik-jane-uuid');
expect(body.user.scopes).toContain('read:tables');
expect(body.user.scopes).toContain('write:tables');
expect(body.user.permissions).toEqual(['read:tables', 'write:tables']);
});
it('cas 4b — JWT sans claim acadenice_permissions[] : auth OK mais scopes vides', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, {
email: 'jane@acadenice.fr',
sub: 'sub-x',
groups: [],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { permissions: string[]; scopes: string[] } };
expect(body.user.permissions).toEqual([]);
expect(body.user.scopes).toEqual([]);
});
it('cas 4c — JWT avec groups Authentik mappes -> scopes via groupsScopesMap', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
groupsScopesMap: { 'role-formateur': ['read:tables'] },
});
const token = await signJwt(jwks, {
email: 'fmt@acadenice.fr',
groups: ['role-formateur'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { scopes: string[]; groups: string[] } };
expect(body.user.scopes).toContain('read:tables');
expect(body.user.groups).toContain('role-formateur');
});
it('cas 4d — union groups + permissions explicites', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
groupsScopesMap: { 'role-x': ['read:tables'] },
});
const token = await signJwt(jwks, {
email: 'x@acadenice.fr',
groups: ['role-x'],
acadenice_permissions: ['write:tables'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
const body = (await res.json()) as { user: { scopes: string[] } };
expect(body.user.scopes).toContain('read:tables');
expect(body.user.scopes).toContain('write:tables');
});
it('cas 5 — JWT signature invalide -> 401 AUTH_INVALID', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, { email: 'x@y.z' });
const tampered = `${token.slice(0, -16)}AAAAAAAAAAAAAAAA`;
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${tampered}` },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('cas 6 — JWT expired -> 401', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, { email: 'x@y.z' }, { expiresIn: '-1s' });
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('cas 7 — JWT issuer different -> 401', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, { email: 'x@y.z' }, { issuer: 'https://evil.example/' });
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('cas 8 — JWT audience different -> 401', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, { email: 'x@y.z' }, { audience: 'other-app' });
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('cas 9 — Cookie authToken valid -> 200, source=oidc-cookie', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, {
email: 'cookie@acadenice.fr',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/me', {
headers: { Cookie: `authToken=${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { source: string; email: string } };
expect(body.user.source).toBe('oidc-cookie');
expect(body.user.email).toBe('cookie@acadenice.fr');
});
it('cas 10 — requireScope match via permissions claim -> 200', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, {
email: 'a@b.c',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/needs-read-tables', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { ok: boolean; scopes: string[] };
expect(body.ok).toBe(true);
expect(body.scopes).toContain('read:tables');
});
it('cas 11 — requireScope deny -> 403 FORBIDDEN_SCOPE', async () => {
const { app } = buildApp({ oidcEnabled: true, jwks });
const token = await signJwt(jwks, {
email: 'a@b.c',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/needs-admin', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(403);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('FORBIDDEN_SCOPE');
});
});
// ---------------------------------------------------------------------------
// R2.3b — Mode JWT HMAC DocAdenice
// ---------------------------------------------------------------------------
const DOCMOST_SECRET = randomBytes(48).toString('hex');
async function signDocmostJwt(
claims: Record<string, unknown>,
overrides?: { issuer?: string; audience?: string; expiresIn?: string; secret?: string },
): Promise<string> {
const builder = new SignJWT(claims)
.setProtectedHeader({ alg: 'HS256' })
.setIssuedAt()
.setIssuer(overrides?.issuer ?? 'Docmost')
.setExpirationTime(overrides?.expiresIn ?? '5m');
if (overrides?.audience) builder.setAudience(overrides.audience);
return builder.sign(Buffer.from(overrides?.secret ?? DOCMOST_SECRET, 'utf-8'));
}
describe('auth middleware — JWT HMAC DocAdenice (R2.3b)', () => {
it('Bearer JWT HS256 DocAdenice valide -> 200 + source=docmost-jwt', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt({
sub: 'docmost-user-uuid',
email: 'corentin@acadenice.fr',
workspaceId: 'ws-1',
type: 'access',
sessionId: 'sess-1',
acadenice_permissions: ['read:tables', 'write:tables'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as {
user: {
source: string;
email: string;
sub: string;
scopes: string[];
permissions: string[];
groups: string[];
};
};
expect(body.user.source).toBe('docmost-jwt');
expect(body.user.email).toBe('corentin@acadenice.fr');
expect(body.user.sub).toBe('docmost-user-uuid');
expect(body.user.scopes).toEqual(['read:tables', 'write:tables']);
expect(body.user.permissions).toEqual(['read:tables', 'write:tables']);
expect(body.user.groups).toEqual([]);
});
it('Bearer JWT HS256 sans permissions claim -> auth OK + scopes vides', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt({
sub: 'u',
workspaceId: 'w',
type: 'access',
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { scopes: string[]; permissions: string[] } };
expect(body.user.scopes).toEqual([]);
expect(body.user.permissions).toEqual([]);
});
it('Bearer JWT HS256 mais DocAdenice mode desactive -> 401 AUTH_INVALID', async () => {
const { app } = buildApp({ oidcEnabled: false });
const token = await signDocmostJwt({
sub: 'u',
workspaceId: 'w',
type: 'access',
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('Cookie authToken HS256 DocAdenice -> 200 + source=docmost-cookie', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt({
sub: 'cookie-user',
email: 'cookie@acadenice.fr',
workspaceId: 'ws-1',
type: 'access',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/me', {
headers: { Cookie: `authToken=${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { source: string; email: string } };
expect(body.user.source).toBe('docmost-cookie');
expect(body.user.email).toBe('cookie@acadenice.fr');
});
it('JWT HS256 expired -> 401', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt(
{ sub: 'u', workspaceId: 'w', type: 'access' },
{ expiresIn: '-1s' },
);
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('JWT HS256 wrong signature (mauvais secret) -> 401', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt(
{ sub: 'u', workspaceId: 'w', type: 'access' },
{ secret: randomBytes(48).toString('hex') },
);
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('JWT HS256 issuer mismatch -> 401', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt(
{ sub: 'u', workspaceId: 'w', type: 'access' },
{ issuer: 'Acme' },
);
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
});
it('JWT HS256 + DocAdenice permissions -> requireScope match -> 200', async () => {
const { app } = buildApp({ oidcEnabled: false, docmostSecret: DOCMOST_SECRET });
const token = await signDocmostJwt({
sub: 'u',
workspaceId: 'w',
type: 'access',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/needs-read-tables', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
});
});
describe('auth middleware — coexistence Authentik + DocAdenice (R2.3b)', () => {
let jwks: JwksFixture;
beforeAll(async () => {
jwks = await startJwksServer();
});
afterAll(async () => {
await new Promise<void>((resolve) => jwks.server.close(() => resolve()));
});
it('JWT RS256 Authentik valide -> route vers OIDC (mode 2)', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
docmostSecret: DOCMOST_SECRET,
});
const token = await signJwt(jwks, {
email: 'jane@acadenice.fr',
sub: 'authentik-jane',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { source: string } };
expect(body.user.source).toBe('oidc-jwt');
});
it('JWT HS256 DocAdenice valide -> route vers DocAdenice (mode 3)', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
docmostSecret: DOCMOST_SECRET,
});
const token = await signDocmostJwt({
sub: 'docmost-user',
workspaceId: 'ws-1',
type: 'access',
acadenice_permissions: ['read:tables'],
});
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(200);
const body = (await res.json()) as { user: { source: string } };
expect(body.user.source).toBe('docmost-jwt');
});
it('JWT RS256 mais OIDC desactive (DocAdenice seul) -> 401 (algo mismatch)', async () => {
const { app } = buildApp({
oidcEnabled: false,
docmostSecret: DOCMOST_SECRET,
});
const token = await signJwt(jwks, { email: 'x@y.z' });
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${token}` },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('JWT avec algo none -> 401 (algo non supporte)', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
docmostSecret: DOCMOST_SECRET,
});
// header algo "none" — manuellement forge.
const header = Buffer.from(JSON.stringify({ alg: 'none', typ: 'JWT' }), 'utf-8').toString(
'base64url',
);
const payload = Buffer.from(JSON.stringify({ sub: 'u' }), 'utf-8').toString('base64url');
const noneJwt = `${header}.${payload}.`;
const res = await app.request('/protected/me', {
headers: { Authorization: `Bearer ${noneJwt}` },
});
expect(res.status).toBe(401);
const body = (await res.json()) as { error: { code: string } };
expect(body.error.code).toBe('AUTH_INVALID');
});
it('JWT garbage (header non decodable) -> 401', async () => {
const { app } = buildApp({
oidcEnabled: true,
jwks,
docmostSecret: DOCMOST_SECRET,
});
const res = await app.request('/protected/me', {
headers: { Authorization: 'Bearer notajwt.atall.bro' },
});
expect(res.status).toBe(401);
});
});