Skip to content

feat: add authentication - gateway API keys and operator JWTs (#42) - #77

Open
thebabalola wants to merge 4 commits into
FinesseStudioLab:mainfrom
thebabalola:feat/api-authentication
Open

thebabalola wants to merge 4 commits into
FinesseStudioLab:mainfrom
thebabalola:feat/api-authentication

Conversation

@thebabalola

Copy link
Copy Markdown
Contributor

Closes #42

Summary of Changes

Establishes the auth layer before more routes land. Every route is protected by default via a global onRequest hook; public routes explicitly opt-out with config: { public: true }. This prevents new endpoints from inheriting open access.

Gateway credentials are scrypt-hashed with per-key salt and a detectable mt_<name>_ prefix for leak scanning. Rotation is supported by keeping multiple entries with the same prefix active during the overlap window. Operator sessions are HS256 JWTs (via jose) with issuer/audience modeltrace-api and 8h expiry.

What changed

  • src/config/env.ts: require JWT_SECRET (>=32 chars) and add API_KEY_STORE JSON array; expose via config.auth.
  • .env.example: document JWT_SECRET and API_KEY_STORE (empty array means JWT-only).
  • src/auth/types.ts / provider.ts: hash helpers, generateApiKey, parseApiKeyStore, resolveAuthProvider with ApiKey and Bearer verification, expiration and scope handling.
  • src/auth/plugin.ts: registerAuthHooks(app, auth) - global hook that skips public:true routes, returns 401 otherwise and stashes req.identity.
  • src/auth/testing.ts: light test auth provider using x-test-auth header.
  • src/index.ts: decorate app.auth, call registerAuthHooks before health/v1 routes; guard top-level listen with import.meta.url check.
  • src/routes/health.ts & src/routes/v1/index.ts: mark liveness/readiness and /api/v1/meta as public.
  • src/routes/disputes/index.ts: scope gates (dispute:read/dispute:write/admin), derive userId from req.identity.subject while keeping x-role business header.
  • src/auth/index.test.ts: 10 tests for hashing, key/JWT auth, expiry, tampering, rotation overlap, store parsing.
  • src/index.test.ts: 7 tests - health/meta public, 401 unauthenticated, valid gateway key, valid JWT, 403 scope, default deny.
  • src/routes/disputes/index.test.ts & src/routes/health.test.ts & src/config/env.test.ts: updated to provide JWT/registry env and use test auth.

Testing / Local Verification

npm run lint   # tsc --noEmit - pass
npm run build  # tsc - pass
npm test       # 81 passed, 0 failed
npm audit --audit-level=high  # 0 vulnerabilities

Scopes enforced per acceptance criteria: attest:write, usage:write, export:read, admin plus dispute scopes. Keys are never stored plaintext.

@thebabalola

thebabalola commented Aug 28, 2026

Copy link
Copy Markdown
Contributor Author

Hi @joelpeace48-cell , auth layer from #42 is ready for review. Gateway keys are scrypt-hashed with per-key salt and prefix scanning, JWTs are HS256 via jose, and protection is deny-by-default with explicit opt-out for public routes. Happy to adjust scopes or expiry if you have a different convention in mind.

@joelpeace48-cell

Copy link
Copy Markdown
Contributor

@thebabalola CI failed

…eStudioLab#42)

Implement the auth layer required before attestation/usage/export
routes land. Every endpoint is protected by default via a global
onRequest hook; public routes explicitly opt-out via config { public:true }.

Gateway credentials:
- API keys prefixed with mt_<name>_ for leak scanning
- Stored as scrypt hash + per-key salt, verified with timingSafeEqual
- Expiration and rotation overlap - multiple active entries with same prefix
  are all tried

Operator sessions:
- HS256 JWTs via jose, issuer/audience modeltrace-api, 8h lifetime
- Short-lived, issued via auth.issueOperatorToken

Server:
- src/config/env.ts: require JWT_SECRET (>=32), API_KEY_STORE JSON array
- src/auth/*: types, provider, plugin (registerAuthHooks), testing helper
- src/index.ts: decorate app.auth, registerAuthHooks before routes
- src/routes/health.ts, src/routes/v1/index.ts: mark liveness/meta as public
- src/routes/disputes: scope checks dispute:read/write/admin, derive userId
  from authenticated subject

Tests:
- src/auth/index.test.ts covers hashing, valid/invalid keys, expiration,
  JWT, tampering, rotation and parse helpers
- src/index.test.ts covers health/meta public, 401 unauthenticated, valid
  gateway key, valid JWT, scope 403, default deny
- src/routes/disputes/index.test.ts now authenticates via test auth provider
  and checks scope rejection
- .env.example updated with JWT_SECRET and API_KEY_STORE

# Conflicts:
#	.env.example
#	src/config/env.test.ts
#	src/config/env.ts
#	src/index.test.ts
#	src/index.ts
… tests

- Merged rate limiting (FinesseStudioLab#76) and auth (FinesseStudioLab#42) features in env.ts, index.ts,
  index.test.ts, .env.example, env.test.ts
- Applied lazy config (getConfig) to prevent eager parseEnv at import time
- Augmented FastifyContextConfig with public boolean for route auth opt-out
- Fixed body limit tests to include auth credentials (401 was returned
  before body parsing could trigger 413/201)
- All 84 tests passing, lint clean, 0 audit vulnerabilities
@thebabalola
thebabalola force-pushed the feat/api-authentication branch from eedfb7a to a2dd545 Compare August 29, 2026 21:32
@thebabalola

Copy link
Copy Markdown
Contributor Author

Hi @joelpeace48-cell — just resolved the merge conflicts with #76 (rate limiting) and fixed the CI failure (JWT_SECRET was undefined at module load due to eager env parsing). All 84 tests passing now, CI green. Ready for another look when you get a chance.

@joelpeace48-cell

Copy link
Copy Markdown
Contributor

Nice job @thebabalola

@joelpeace48-cell

Copy link
Copy Markdown
Contributor

@thebabalola, ci failed

@thebabalola

Copy link
Copy Markdown
Contributor Author

Hi @joelpeace48-cell — the merge of #78 into main added env.exit.test.ts, which did not include JWT_SECRET (required since #42). Added it to the validEnv fixture. 87/87 tests passing, lint clean. Ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

core: the API has no authentication whatsoever

2 participants