Skip to content

Authentication & Authorization Model

Companion to auth-boundaries. The boundaries doc states the rule per service; this doc explains the mechanism: who issues tokens, who verifies them, where authorization decisions are made, and how services trust each other.

Identity providers

Two identity sources are in use:

SourceUsed forVerified by
ClerkAll end-user identity (web + admin)@clerk/backend, @clerk/nextjs, clerk-sdk-go/v2
API keyService-to-service into brainapps/brain/src/modules/application/auth/strategies/api-key.strategy.ts

Clerk issues two token shapes that the verifiers handle differently:

  • JWT session tokens — short-lived, verified locally with the secret key (no Clerk round-trip).
  • OAuth opaque tokens prefixed oat_ — exchanged at ${CLERK_FRONTEND_API_URL}/oauth/userinfo for the underlying sub (see apps/brain/src/modules/application/auth/strategies/clerk.strategy.ts:40-103).

Token verification per service

ServiceEdge verifierLibraryNotes
brisketapps/brisket/src/middleware.ts@clerk/nextjs/server clerkMiddlewarePublic route allowlist; same-origin redirect validation
fennecapps/fennec/src/lib/auth/clerk.ts@clerk/clerk-react; SPA only — Clerk session token is fetched client-side and forwarded to brain (apps/fennec/src/lib/backend/apiClient.ts callers). Token verification happens in brain via @clerk/backend, not in fennecAdmin SPA
stripapps/strip/internal/app/middleware/auth.goclerk-sdk-go/v2 via services.ClerkServiceCustom Fiber middleware; 10s Clerk timeout, 8KB token cap
brainapps/brain/src/modules/application/auth/guards/clerk-auth.guard.ts@clerk/backendNestJS Passport guard; @Public() and API-key escape hatches
flankapps/flank/app/lib/auth.ts@clerk/backend authenticateRequestServer-function-level requireAuth()
sirloinapps/sirloin/internal/pkg/clerk/clerk-sdk-go/v2Used for user lookups, not token validation at the gRPC edge — see Service-to-service auth
roundnoneInternal-only; no auth boundary today

Verification flow

flowchart LR
Browser[User browser] -->|Clerk session JWT or oat_*| Brisket
Browser -->|Clerk session| Fennec
Browser -->|Clerk session| Flank
AdminBrowser[Admin browser] -->|Clerk session JWT| Strip
Brisket -->|REST + user context| Sirloin
Strip -->|gRPC + user_id in request| Sirloin
Flank -->|gRPC + user_id| Sirloin
Sirloin -->|gRPC| Brain
Sirloin -->|API key header| Brain
Sirloin -->|gRPC| Round
Brain -->|gRPC| Round
Sirloin -->|user lookups via clerk-sdk-go| Clerk[(Clerk)]

Authorization decisions

Authorization is not centralized. Each service makes its own decisions:

  • Strip RBAC: apps/strip/internal/app/authorization/authorization.go defines Permission constants and 5 roles — root, admin, creator, support, viewer. Roles are persisted by sirloin (model apps/sirloin/internal/pkg/models/stripadminrole.go) and cached in strip for 5 minutes (apps/strip/internal/app/services/authorization.go). Default for unknown users is viewer.
  • Sirloin per-RPC checks: rate limits and ownership checks live in service code. The rate limiter (apps/sirloin/internal/pkg/ratelimit/ratelimit.go) reads GetUserId() / GetUserEmail() off the request proto and bypasses internal @foxy.ai emails (see Bypass patterns).
  • Brain route metadata: @Public() and @ApiKeyRoute() decorators flip the Clerk guard off per-route (apps/brain/src/modules/application/auth/guards/clerk-auth.guard.ts:21-43). When a route is API-key-only the ApiKeyAuthGuard runs instead.
  • Flank: requireAuth() is the only check — apps/flank/app/lib/auth.ts:17. There is no per-workflow ACL today. FlankMCP role gating lives in sirloin: every tool entry in toolMinimumRole requires admin (apps/sirloin/internal/app/flankmcp/roles.go:30-55), enforced via isToolAllowedForRole(...) at the tool dispatch site (apps/sirloin/internal/app/flankmcp/server.go:295-300). Roles are resolved from the same StripAdminRole source as Strip RBAC.

Service-to-service auth

Caller → CalleeMechanismCode reference
sirloin → brain (HTTP)SIRLOIN_BRAIN_API_KEY shared secret.env.example line SIRLOIN_BRAIN_API_KEY=secretkey1; brain side apps/brain/src/modules/application/auth/strategies/api-key.strategy.ts
sirloin → round (gRPC)No transport securitygrpc.WithTransportCredentials(insecure.NewCredentials())apps/sirloin/cmd/app/main.go:237, :263
sirloin → flank (gRPC, FlankExecutionService)Connect-es over connectNodeAdapter (apps/flank/server/grpc-server.ts:8-20); no auth interceptor is registered on the server router — relies on internal-network trustapps/flank/server/grpc-server.ts
flank → sirloin (gRPC, FlankStorageService)Connect-es createGrpcTransport to SIRLOIN_GRPC_URL (apps/flank/app/lib/grpc-client.ts:6-29); the only interceptor re-throws errors as plain Error — no auth header / token attachedapps/flank/app/lib/grpc-client.ts
brain → round (gRPC)credentials.createInsecure()apps/brain/src/modules/application/round/providers/round-grpc-client.provider.ts:19
sirloin gRPC serverPlain TCP, no TLS configured at serverapps/sirloin/cmd/app/main.go:552 grpc.NewServer(...) — no TLS option

Implication: All inter-service gRPC traffic relies on the network being internal (Docker network in dev, Railway private network in prod — see Security model). There is no mTLS today.

API key scopes

Brain’s API-key strategy validates membership in a flat list (AUTHORIZED_KEYS env var, comma-separated). There are no scopes or per-key permissions; possession of any listed key authorizes any @ApiKeyRoute() endpoint (apps/brain/src/modules/application/auth/strategies/api-key.strategy.ts:12-49).

The key comparison uses Array.includes(...) rather than a constant-time compare (apps/brain/src/modules/application/auth/strategies/api-key.strategy.ts:41). TODO(@pawel): assess timing-attack risk given keys are long random strings, and switch to crypto.timingSafeEqual if a mitigation is warranted.

Bypass patterns (real, found in code)

These bypasses exist and must be tracked because they alter trust:

  • FLANK_AUTH_BYPASS_UUIDapps/flank/app/lib/auth.ts:4,17-20. When set, requireAuth() returns the UUID immediately without contacting Clerk. Documented as local-dev-only but is plain env-driven.
  • Strip dev modeapps/strip/internal/app/middleware/auth.go:79-87 reads cfg.Stage == env.StageDevelopment into devMode. Two distinct bypasses exist:
    1. STRIP_AUTH_BYPASS UUID (env-driven cfg.AuthBypassUUID), accepted via X-Auth-Bypass header or ?auth= query param at auth.go:92-112 regardless of stage; sets userId=test_user, userEmail=test@example.com and logs SECURITY WARNING.
    2. Dev-no-Clerk fallthrough at auth.go:117-125: when am.devMode && am.clerkService == nil the request is admitted as userId=dev_user. This branch should never trigger in staging or production.
  • Internal email bypass on rate limitsapps/sirloin/internal/pkg/ratelimit/ratelimit.go: any caller whose request proto returns an @foxy.ai email skips the limiter (fraud.IsInternalEmail). This is rate-limit only, not authn/authz.
  • Brain @Public() decorator — bypasses Clerk for the route. The only @Public() use sites in code are apps/brain/src/app.controller.ts:10 (GET / hello) and :16 (GET /health). No other public routes were found via grep across apps/brain/src.
  • OPTIONS preflight — brain’s Clerk guard returns true for any OPTIONS request (apps/brain/src/modules/application/auth/guards/clerk-auth.guard.ts:17-19).

Auth boundaries diagram

flowchart TB
subgraph Internet
UB[User browser]
AB[Admin browser]
CB[Chargebee]
PR[Primer]
end
subgraph Trusted internal network
BR[brisket Next.js]
FE[fennec admin]
FL[flank]
ST[strip]
SI[sirloin gRPC + REST]
BN[brain]
RD[round]
end
UB -->|Clerk JWT| BR
UB -->|Clerk JWT| FE
UB -->|Clerk JWT| FL
AB -->|Clerk JWT| ST
CB -->|Basic auth: chargebee_username/pass| SI
PR -->|HMAC X-Signature-Primary| SI
BR -->|REST, user context| SI
ST -->|gRPC, user_id in proto| SI
FL -->|gRPC| SI
SI -->|API key| BN
SI -->|gRPC insecure| RD
BN -->|gRPC insecure| RD

Cross-references

Open TODOs

  • TODO(@pawel): timing-attack analysis for brain AUTHORIZED_KEYS comparison and decision on crypto.timingSafeEqual.
  • TODO(@law): plan to introduce mTLS or any transport encryption on the internal gRPC mesh — none found in code today.