🐛 fix(auth): 收紧 BFF 鉴权并修复内部调用链路

This commit is contained in:
Abner
2026-05-31 19:25:23 +08:00
parent b815225845
commit df2b6ca814
14 changed files with 152 additions and 31 deletions

View File

@@ -103,6 +103,31 @@ describe('APIService', () => {
});
describe('错误处理', () => {
it('应该为每次重试创建新的超时信号', async () => {
const timeoutService = new APIService({ baseURL: 'https://api.test.com', timeout: 5000, retries: 2 });
const firstSignal = { aborted: true };
const secondSignal = { aborted: false };
const createTimeoutSpy = jest.spyOn(timeoutService, 'createTimeoutSignal')
.mockReturnValueOnce({ signal: firstSignal, clear: jest.fn() })
.mockReturnValueOnce({ signal: secondSignal, clear: jest.fn() });
fetch.mockImplementationOnce(() => Promise.reject(new Error('timeout')));
fetch.mockResolvedValueOnce({
ok: true,
headers: { get: () => 'application/json' },
json: () => Promise.resolve({ ok: true })
});
const result = await timeoutService.get('/retry-timeout');
expect(result).toEqual({ ok: true });
expect(fetch).toHaveBeenCalledTimes(2);
expect(createTimeoutSpy).toHaveBeenCalledTimes(2);
expect(fetch.mock.calls[0][1].signal).toBe(firstSignal);
expect(fetch.mock.calls[1][1].signal).toBe(secondSignal);
});
it('应该在 HTTP 错误时抛出异常', async () => {
// mock fetch 返回 error response但重试会多次调用
fetch.mockResolvedValue({

View File

@@ -3,6 +3,7 @@ describe('BFF proxy guardrails', () => {
const OriginalRequest = global.Request;
const OriginalResponse = global.Response;
const OriginalDeno = global.Deno;
const OriginalFetch = global.fetch;
class MockHeaders {
constructor(init = {}) {
@@ -92,6 +93,7 @@ describe('BFF proxy guardrails', () => {
jest.restoreAllMocks();
global.Request = OriginalRequest;
global.Response = OriginalResponse;
global.fetch = OriginalFetch;
global.Deno = OriginalDeno;
});
@@ -106,7 +108,21 @@ describe('BFF proxy guardrails', () => {
expect(response.status).toBe(404);
});
it('blocks missing-origin calls to protected BFF targets', async () => {
it('allows same-origin public GET requests without Origin', async () => {
const mod = await import('../../netlify/edge-functions/bff-proxy.js');
const request = new Request('https://example.com/bff/public-config', {
method: 'GET'
});
const response = await mod.default(request);
expect(response.status).toBe(200);
expect(global.fetch).toHaveBeenCalledTimes(1);
});
it('returns 403 for missing-origin protected POST requests', async () => {
const mod = await import('../../netlify/edge-functions/bff-proxy.js');
const request = new Request('https://example.com/bff/verify-cookie', {
method: 'POST',
@@ -115,7 +131,9 @@ describe('BFF proxy guardrails', () => {
});
const response = await mod.default(request);
expect(response.status).toBe(403);
expect(response.headers.get('access-control-allow-origin')).toBe('https://esim.cosr.eu.org');
});
it('forwards allowlisted BFF targets with the server x-esim-key', async () => {

View File

@@ -49,6 +49,32 @@ describe('Functions authentication hardening', () => {
expect(result.authorized).toBe(true);
});
it('allows OPTIONS preflight for allowed origin', () => {
const { middleware } = loadModules();
const result = middleware.authenticate({
httpMethod: 'OPTIONS',
headers: { origin: 'https://esim.cosr.eu.org' },
body: '{}',
queryStringParameters: {}
});
expect(result.preflight).toBe(true);
expect(result.origin).toBe('https://esim.cosr.eu.org');
});
it('rejects OPTIONS preflight for disallowed origin', () => {
const { middleware } = loadModules();
expect(() => middleware.authenticate({
httpMethod: 'OPTIONS',
headers: { origin: 'https://evil.example.com' },
body: '{}',
queryStringParameters: {}
})).toThrow('Origin not allowed');
});
it('does not accept authKey from query or body', () => {
const { middleware } = loadModules();
const event = {
@@ -87,6 +113,27 @@ describe('Functions authentication hardening', () => {
expect(form).not.toContain('attacker.example');
});
it('throws a configuration error when ACCESS_KEY is missing for token exchange', async () => {
const { tokenExchange } = loadModules({ ACCESS_KEY: '' });
const result = await tokenExchange.handler({
httpMethod: 'POST',
headers: {
origin: 'https://esim.cosr.eu.org'
},
body: JSON.stringify({
code: 'authorization-code',
code_verifier: 'a'.repeat(64)
}),
queryStringParameters: {}
}, { functionName: 'giffgaff-token-exchange' });
expect(result.statusCode).toBe(500);
expect(result.body).toContain('ACCESS_KEY');
});
it('uses configured GIFFGAFF_REDIRECT_URI for token exchange', async () => {
const { axios, tokenExchange } = loadModules({
GIFFGAFF_REDIRECT_URI: 'giffgaff://custom/callback/'

View File

@@ -24,7 +24,7 @@ describe('Local server route coverage', () => {
const app = require('../../server.js');
const routes = app.locals.bffRoutes;
expect(routes).toEqual(expect.arrayContaining([
const expectedRoutes = [
'/bff/giffgaff-token-exchange',
'/bff/giffgaff-graphql',
'/bff/giffgaff-mfa-challenge',
@@ -33,6 +33,8 @@ describe('Local server route coverage', () => {
'/bff/auto-activate-esim',
'/bff/verify-cookie',
'/bff/public-config'
]));
];
expect([...routes].sort()).toEqual([...expectedRoutes].sort());
});
});