Skip to content

feat(auth-permission): accept a session token as an alternative to x-api-key - #66

Merged
GabrielMachado11 merged 6 commits into
mainfrom
feat/session-token-auth
Sep 16, 2026
Merged

GabrielMachado11 merged 6 commits into
mainfrom
feat/session-token-auth

Conversation

@GabrielMachado11

Copy link
Copy Markdown
Contributor

Summary

  • RouteProperties gains acceptsSessionToken: routes that opt in accept Authorization: Bearer <sessionToken> in addition to x-api-key.
  • The bearer token is verified offline — no call to wave-auth-api — via WaveAuthSessionTokenVerifier, which checks the signature against wave-auth-api's JWKS (jose, RS256/ES256 only, explicit algorithm allowlist against alg-confusion) and expiry, then reads sub/brokers.
  • No permission check runs on this path: wave-auth-api's session tokens deliberately carry no permissions claim (a token must not survive key revocation as an authorization decision), and /auth/permissions/validate itself has no bearer-token path. A route's acceptsSessionToken: true is the authorization decision — it trusts the token's broker scope only.
  • x-broker-id narrows the token's broker scope the same way it narrows an API key's, and the existing single-broker ambiguity check applies identically.
  • registerPermissionAuth fails fast at registration time if a route sets acceptsSessionToken without a sessionTokenVerifier configured.
  • Fully additive: every existing x-api-key caller and route is unaffected.

Test plan

  • pnpm run lint
  • pnpm test (213/213 passing, including new wave-auth-session-token-verifier.spec.ts and the session tokens block in register-permission-auth.spec.ts — valid/expired/wrong-key/missing-claims/HS256-confusion/malformed-JWT cases, plus route opt-in, broker narrowing, and the fail-fast config check)
  • pnpm run build
  • Wiring a real consumer route (e.g. in tim-network-adapter) to acceptsSessionToken is a separate follow-up once a concrete caller needs it

🤖 Generated with Claude Code

…lternative to x-api-key

Routes opt in via RouteProperties.acceptsSessionToken; the Authorization:
Bearer JWT is verified offline against wave-auth-api's JWKS (jose,
RS256/ES256 only) and its brokers claim becomes the resolved broker scope.
No permission check runs on this path — wave-auth-api's session tokens
deliberately carry no permissions claim, so the route's own opt-in is the
authorization decision. Every existing x-api-key caller is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Comment on lines +275 to +284
await checkRoutePermission(
request,
route,
apiKey,
options.validator,
brokerIdHeader,
brokerIdMaxLength,
);
return;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Este arquivo está muito granade e difícil de ser lido, creio que a função checkRoutePermission e suas funções auxiliares podem ficar em um arquivo apartado.
também a renomearia para deixar claro que seu contexto é de API Key

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Movido — checkRoutePermission renomeado para checkApiKeyPermission em check-api-key-permission.ts. A lógica compartilhada com o path de session token foi para broker-scope-resolution.ts.

brokerIdMaxLength,
);
await assertHasPermission(request, route);
});

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O code scanning achou o seguinte erro:
This route handler performs authorization, but is not rate-limited.
This route handler performs authorization, but is not rate-limited.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rate limiting fica fora deste hook de propósito: ele roda dentro de um framework compartilhado usado por vários serviços com perfis de tráfego bem diferentes, então um limite fixo aqui seria arbitrário. Documentei essa decisão no JSDoc de registerPermissionAuth — o rate limiting é esperado na camada de infra/gateway de cada serviço consumidor, o mesmo padrão que o próprio wave-auth-api usa (Cloud Armor via IaC), não em código de aplicação.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Atualização: adicionei rate limiting de fato, opt-in via options.rateLimit (usa @fastify/rate-limit, agrupado por credencial em vez de IP por padrão). Segue sem valor padrão — continua sendo opt-in pelo mesmo motivo (perfis de tráfego diferentes por serviço).

GabrielMachado11 and others added 5 commits September 16, 2026 12:01
…eview feedback

Moves the API-key check (renamed checkApiKeyPermission, its context wasn't
clear before) and the session-token check into their own files, and pulls
their shared broker-scope logic into broker-scope-resolution.ts, so the hook
file itself is left as pure orchestration. Also documents why this hook does
no rate limiting of its own — that's left to each consuming service's
infrastructure/gateway, the same layer wave-auth-api itself relies on.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds @fastify/rate-limit as a dependency and an optional rateLimit option on
registerPermissionAuth, bucketed by the presented credential (API key or
session token) rather than IP by default, so a leaked credential can't roam
free across IPs and one shared NAT doesn't throttle unrelated callers
together. A request over budget throws the new TooManyRequestsError.

Uses @fastify/rate-limit's low-level createRateLimit, not its automatic
per-route wiring (global: true): that wiring only reaches routes registered
after the plugin finishes booting, and registerPermissionAuth is
synchronous, so a caller's very next line registering its own routes would
silently never get rate-limited. createRateLimit is read lazily on the
first real request instead, once .ready() has necessarily already resolved.

Left off by default: this hook runs inside a shared framework with no
single traffic profile that fits every consuming service, and several
already rate-limit at their own infra/gateway layer instead.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Some credentials (API keys or session tokens) authorize a whole tenant
rather than a subset of its brokers. A route can now opt out of broker
scope resolution entirely with brokerScoped: false — x-broker-id is not
read, an empty broker scope on the verdict/token is not treated as a
denial, and request.brokerContext is left unset.

Also replaces the ??= lazy-init in rate-limit-check.ts's createRateLimitCheck
with a plain if-block for a more conventional read.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The directory had grown to ~20 flat files across two credential paths, a
shared rate-limit feature and generic header helpers. Groups them into
api-key/, session-token/, rate-limit/ and shared/, leaving only the public
surface (register-permission-auth.ts, route-properties.ts, broker-context.ts,
index.ts) and errors/ at the top level. No behavior change — only import
paths moved; the package's own @wave-tech/framework/auth-permission export
is unaffected.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@GabrielMachado11
GabrielMachado11 merged commit bf77303 into main Sep 16, 2026
6 checks passed
@GabrielMachado11
GabrielMachado11 deleted the feat/session-token-auth branch September 16, 2026 16:43
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.

1 participant