mirror of
https://github.com/Sea-Haven-Industries/sh-mcp.git
synced 2026-10-02 02:33:18 +00:00
119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
|
|
/**
|
||
|
|
* LocalAuthProvider — local-auth safety + audience binding (build-plan §3, §5).
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { describe, it, expect } from 'vitest';
|
||
|
|
|
||
|
|
import {
|
||
|
|
LocalAuthProvider,
|
||
|
|
defaultLocalPrincipals,
|
||
|
|
OPS_AUDIENCE,
|
||
|
|
FINANCE_AUDIENCE,
|
||
|
|
} from './local-auth.js';
|
||
|
|
import { AuthError } from './cognito-auth.js';
|
||
|
|
|
||
|
|
function bearer(token: string) {
|
||
|
|
return { headers: { authorization: `Bearer ${token}` } };
|
||
|
|
}
|
||
|
|
|
||
|
|
describe('LocalAuthProvider safety', () => {
|
||
|
|
it('refuses to construct inside an AWS runtime even when env=local', () => {
|
||
|
|
for (const v of ['AWS_LAMBDA_FUNCTION_NAME', 'AWS_EXECUTION_ENV']) {
|
||
|
|
const prev = process.env[v];
|
||
|
|
process.env[v] = 'sh-mcp-finance';
|
||
|
|
try {
|
||
|
|
expect(
|
||
|
|
() =>
|
||
|
|
new LocalAuthProvider({
|
||
|
|
audience: OPS_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: 'local',
|
||
|
|
}),
|
||
|
|
).toThrow(/refuses to run inside an AWS/);
|
||
|
|
} finally {
|
||
|
|
if (prev === undefined) delete process.env[v];
|
||
|
|
else process.env[v] = prev;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
it('refuses to construct unless SH_MCP_ENV=local', () => {
|
||
|
|
expect(
|
||
|
|
() =>
|
||
|
|
new LocalAuthProvider({
|
||
|
|
audience: OPS_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: 'aws',
|
||
|
|
}),
|
||
|
|
).toThrow(/only be constructed when SH_MCP_ENV=local/);
|
||
|
|
expect(
|
||
|
|
() =>
|
||
|
|
new LocalAuthProvider({
|
||
|
|
audience: OPS_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: undefined,
|
||
|
|
}),
|
||
|
|
).toThrow();
|
||
|
|
});
|
||
|
|
|
||
|
|
it('constructs in local mode', () => {
|
||
|
|
expect(
|
||
|
|
() =>
|
||
|
|
new LocalAuthProvider({
|
||
|
|
audience: OPS_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: 'local',
|
||
|
|
}),
|
||
|
|
).not.toThrow();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
describe('LocalAuthProvider authentication', () => {
|
||
|
|
const ops = new LocalAuthProvider({
|
||
|
|
audience: OPS_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: 'local',
|
||
|
|
});
|
||
|
|
const finance = new LocalAuthProvider({
|
||
|
|
audience: FINANCE_AUDIENCE,
|
||
|
|
principals: defaultLocalPrincipals(),
|
||
|
|
env: 'local',
|
||
|
|
});
|
||
|
|
|
||
|
|
it('resolves a known dev token to the right AuthContext', async () => {
|
||
|
|
const ctx = await ops.authenticate(bearer('dev-ops-only'));
|
||
|
|
expect(ctx.sub).toBe('ops-only@seahavenind.com');
|
||
|
|
expect(ctx.scopes).toContain('ops:read');
|
||
|
|
expect(ctx.aud).toBe(OPS_AUDIENCE);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects a missing token (→401)', async () => {
|
||
|
|
await expect(ops.authenticate({ headers: {} })).rejects.toMatchObject({
|
||
|
|
name: 'AuthError',
|
||
|
|
code: 'missing_token',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('rejects an unknown token (→401)', async () => {
|
||
|
|
await expect(ops.authenticate(bearer('not-a-real-token'))).rejects.toBeInstanceOf(AuthError);
|
||
|
|
});
|
||
|
|
|
||
|
|
it('AUDIENCE BINDING: an ops principal is rejected by the finance server', async () => {
|
||
|
|
await expect(finance.authenticate(bearer('dev-ops-only'))).rejects.toMatchObject({
|
||
|
|
code: 'client_not_allowed',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('AUDIENCE BINDING: a finance principal is rejected by the ops server', async () => {
|
||
|
|
await expect(ops.authenticate(bearer('dev-finance'))).rejects.toMatchObject({
|
||
|
|
code: 'client_not_allowed',
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
it('the finance principal authenticates against the finance server', async () => {
|
||
|
|
const ctx = await finance.authenticate(bearer('dev-finance'));
|
||
|
|
expect(ctx.scopes).toContain('finance:read');
|
||
|
|
expect(ctx.aud).toBe(FINANCE_AUDIENCE);
|
||
|
|
});
|
||
|
|
});
|