86 lines
3.4 KiB
TypeScript
86 lines
3.4 KiB
TypeScript
import { Hono } from 'hono';
|
||
import { beforeAll, describe, expect, it } from 'vitest';
|
||
import { makeToken, setupTestJwks } from '../support/token';
|
||
import { requireAuth, requireRealmRole, requireService } from '../../src/shared/keycloak-auth';
|
||
|
||
function appWith(...mw: Parameters<Hono['use']>[1][]): Hono {
|
||
const app = new Hono();
|
||
for (const m of mw) app.use('/p/*', m);
|
||
app.get('/p/x', (c) => c.json({ user: c.get('user') }));
|
||
return app;
|
||
}
|
||
|
||
describe('keycloak-auth', () => {
|
||
beforeAll(async () => {
|
||
await setupTestJwks();
|
||
});
|
||
|
||
it('缺 Bearer → 401 unauthorized', async () => {
|
||
const res = await appWith(requireAuth()).request('/p/x');
|
||
expect(res.status).toBe(401);
|
||
expect(((await res.json()) as { error: string }).error).toBe('unauthorized');
|
||
});
|
||
|
||
it('合法 token → 200 且 user 就位', async () => {
|
||
const token = await makeToken({ sub: 'u-1', roles: ['user'] });
|
||
const res = await appWith(requireAuth()).request('/p/x', {
|
||
headers: { authorization: `Bearer ${token}` },
|
||
});
|
||
expect(res.status).toBe(200);
|
||
const body = (await res.json()) as { user: { id: string; roles: string[] } };
|
||
expect(body.user.id).toBe('u-1');
|
||
expect(body.user.roles).toContain('user');
|
||
});
|
||
|
||
it('aud 不含本服务 → 401 invalid_token 且 message 点名 audience mapper', async () => {
|
||
const token = await makeToken({ aud: 'some-other-service' });
|
||
const res = await appWith(requireAuth()).request('/p/x', {
|
||
headers: { authorization: `Bearer ${token}` },
|
||
});
|
||
expect(res.status).toBe(401);
|
||
expect(((await res.json()) as { message: string }).message).toContain('audience mapper');
|
||
});
|
||
|
||
it('过期 token → 401 且 message 为 token 已过期', async () => {
|
||
const token = await makeToken({ expiresIn: -60 });
|
||
const res = await appWith(requireAuth()).request('/p/x', {
|
||
headers: { authorization: `Bearer ${token}` },
|
||
});
|
||
expect(res.status).toBe(401);
|
||
expect(((await res.json()) as { message: string }).message).toContain('过期');
|
||
});
|
||
|
||
it('角色不够 → 403(401/403 语义分明)', async () => {
|
||
const token = await makeToken({ roles: ['user'] });
|
||
const res = await appWith(requireAuth(), requireRealmRole('admin')).request('/p/x', {
|
||
headers: { authorization: `Bearer ${token}` },
|
||
});
|
||
expect(res.status).toBe(403);
|
||
});
|
||
|
||
it('admin 通过 requireRealmRole(user, admin) 任一命中', async () => {
|
||
const token = await makeToken({ roles: ['admin'] });
|
||
const res = await appWith(requireAuth(), requireRealmRole('user', 'admin')).request('/p/x', {
|
||
headers: { authorization: `Bearer ${token}` },
|
||
});
|
||
expect(res.status).toBe(200);
|
||
});
|
||
|
||
it('requireService:service 角色+azp 白名单', async () => {
|
||
const svc = await makeToken({
|
||
roles: ['service'], azp: 'task-dispatcher', username: 'service-account-task-dispatcher',
|
||
});
|
||
const ok = await appWith(requireAuth(), requireService(['task-dispatcher'])).request('/p/x', {
|
||
headers: { authorization: `Bearer ${svc}` },
|
||
});
|
||
expect(ok.status).toBe(200);
|
||
const wrongAzp = await makeToken({
|
||
roles: ['service'], azp: 'agent-service', username: 'service-account-agent-service',
|
||
});
|
||
const deny = await appWith(requireAuth(), requireService(['task-dispatcher'])).request('/p/x', {
|
||
headers: { authorization: `Bearer ${wrongAzp}` },
|
||
});
|
||
expect(deny.status).toBe(403);
|
||
});
|
||
});
|