mirror of
https://github.com/Silentely/eSIM-Tools.git
synced 2026-09-03 14:30:18 +08:00
163 lines
4.8 KiB
JavaScript
163 lines
4.8 KiB
JavaScript
describe('Functions authentication hardening', () => {
|
|
const originalEnv = process.env;
|
|
|
|
function loadModules(envOverrides = {}) {
|
|
jest.resetModules();
|
|
process.env = {
|
|
...originalEnv,
|
|
ACCESS_KEY: 'test-access-key',
|
|
ALLOWED_ORIGIN: 'https://esim.cosr.eu.org',
|
|
GIFFGAFF_CLIENT_ID: 'client-id',
|
|
GIFFGAFF_CLIENT_SECRET: 'abcdefghijklmnopqrstuvwxyzABCDEF',
|
|
GIFFGAFF_REDIRECT_URI: 'giffgaff://auth/callback/',
|
|
...envOverrides
|
|
};
|
|
|
|
return {
|
|
axios: require('axios'),
|
|
middleware: require('../../netlify/functions/_shared/middleware'),
|
|
tokenExchange: require('../../netlify/functions/giffgaff-token-exchange')
|
|
};
|
|
}
|
|
|
|
afterEach(() => {
|
|
process.env = originalEnv;
|
|
jest.restoreAllMocks();
|
|
});
|
|
|
|
it('rejects protected browserless calls without internal key', () => {
|
|
const { middleware } = loadModules();
|
|
|
|
expect(() => middleware.authenticate({
|
|
httpMethod: 'POST',
|
|
headers: {},
|
|
body: '{}',
|
|
queryStringParameters: {}
|
|
})).toThrow('Origin not allowed');
|
|
});
|
|
|
|
it('allows protected browserless internal calls with x-esim-key', () => {
|
|
const { middleware } = loadModules();
|
|
|
|
const result = middleware.authenticate({
|
|
httpMethod: 'POST',
|
|
headers: { 'x-esim-key': 'test-access-key' },
|
|
body: '{}',
|
|
queryStringParameters: {}
|
|
});
|
|
|
|
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 = {
|
|
httpMethod: 'POST',
|
|
headers: { origin: 'https://esim.cosr.eu.org' },
|
|
body: JSON.stringify({ authKey: 'test-access-key' }),
|
|
queryStringParameters: { authKey: 'test-access-key' }
|
|
};
|
|
|
|
expect(() => middleware.authenticate(event)).toThrow('Unauthorized');
|
|
});
|
|
|
|
it('uses server redirect URI for token exchange even if client sends another value', async () => {
|
|
const { axios, tokenExchange } = loadModules();
|
|
jest.spyOn(axios, 'post').mockResolvedValue({
|
|
data: { access_token: 'token' }
|
|
});
|
|
|
|
const result = await tokenExchange.handler({
|
|
httpMethod: 'POST',
|
|
headers: {
|
|
origin: 'https://esim.cosr.eu.org',
|
|
'x-esim-key': 'test-access-key'
|
|
},
|
|
body: JSON.stringify({
|
|
code: 'authorization-code',
|
|
code_verifier: 'a'.repeat(64),
|
|
redirect_uri: 'https://attacker.example/callback'
|
|
}),
|
|
queryStringParameters: {}
|
|
}, { functionName: 'giffgaff-token-exchange' });
|
|
|
|
expect(result.statusCode).toBe(200);
|
|
const form = axios.post.mock.calls[0][1];
|
|
expect(form).toContain('redirect_uri=giffgaff%3A%2F%2Fauth%2Fcallback%2F');
|
|
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/'
|
|
});
|
|
jest.spyOn(axios, 'post').mockResolvedValue({
|
|
data: { access_token: 'token' }
|
|
});
|
|
|
|
const result = await tokenExchange.handler({
|
|
httpMethod: 'POST',
|
|
headers: {
|
|
origin: 'https://esim.cosr.eu.org',
|
|
'x-esim-key': 'test-access-key'
|
|
},
|
|
body: JSON.stringify({
|
|
code: 'authorization-code',
|
|
code_verifier: 'a'.repeat(64)
|
|
}),
|
|
queryStringParameters: {}
|
|
}, { functionName: 'giffgaff-token-exchange' });
|
|
|
|
expect(result.statusCode).toBe(200);
|
|
const form = axios.post.mock.calls[0][1];
|
|
expect(form).toContain('redirect_uri=giffgaff%3A%2F%2Fcustom%2Fcallback%2F');
|
|
});
|
|
});
|