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
Section titled “Identity providers”Two identity sources are in use:
| Source | Used for | Verified by |
|---|---|---|
| Clerk | All end-user identity (web + admin) | @clerk/backend, @clerk/nextjs, clerk-sdk-go/v2 |
| API key | Service-to-service into brain | apps/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/userinfofor the underlyingsub(seeapps/brain/src/modules/application/auth/strategies/clerk.strategy.ts:40-103).
Token verification per service
Section titled “Token verification per service”| Service | Edge verifier | Library | Notes |
|---|---|---|---|
| brisket | apps/brisket/src/middleware.ts |
@clerk/nextjs/server clerkMiddleware |
Public route allowlist; same-origin redirect validation |
| fennec | apps/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 fennec |
Admin SPA |
| strip | apps/strip/internal/app/middleware/auth.go |
clerk-sdk-go/v2 via services.ClerkService |
Custom Fiber middleware; 10s Clerk timeout, 8KB token cap |
| brain | apps/brain/src/modules/application/auth/guards/clerk-auth.guard.ts |
@clerk/backend |
NestJS Passport guard; @Public() and API-key escape hatches |
| flank | apps/flank/app/lib/auth.ts |
@clerk/backend authenticateRequest |
Server-function-level requireAuth() |
| sirloin | apps/sirloin/internal/pkg/clerk/ |
clerk-sdk-go/v2 |
Used for user lookups, not token validation at the gRPC edge — see Service-to-service auth |
| round | none | — | Internal-only; no auth boundary today |
Verification flow
Section titled “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 -->|HTTP + Clerk JWT| Brain Sirloin -->|API key header| Brain Sirloin -->|gRPC| Round Brain -->|gRPC| Round Sirloin -->|user lookups via clerk-sdk-go| Clerk[(Clerk)]
Authorization decisions
Section titled “Authorization decisions”Authorization is not centralized. Each service makes its own decisions:
- Strip RBAC:
apps/strip/internal/app/authorization/authorization.godefinesPermissionconstants and 5 roles —root,admin,creator,support,viewer. Roles are persisted by sirloin (modelapps/sirloin/internal/pkg/models/stripadminrole.go) and cached in strip for 5 minutes (apps/strip/internal/app/services/authorization.go). Default for unknown users isviewer. - Sirloin per-RPC checks: rate limits and ownership checks live in
service code. The rate limiter
(
apps/sirloin/internal/pkg/ratelimit/ratelimit.go) readsGetUserId()/GetUserEmail()off the request proto and bypasses internal@foxy.aiemails (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 theApiKeyAuthGuardruns instead. - Flank:
requireAuth()is the edge check (apps/flank/app/lib/auth.ts); a per-request Clerk JWT is forwarded to brain, which gates workflow operations behind the ADMIN role. The legacy sirloin-hosted FlankMCP tooling (and its role gating) has been removed.
Service-to-service auth
Section titled “Service-to-service auth”| Caller → Callee | Mechanism | Code 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 → brain character runtime (gRPC) | No per-call credential; relies on private-network reachability | apps/sirloin/internal/app/services/characterruntimebrain.go |
| brain → sirloin character completion (HTTP) | X-Webhook-Secret shared secret |
character-completion webhook (same pattern as media-completion) |
| sirloin → round (gRPC) | No transport security — grpc.WithTransportCredentials(insecure.NewCredentials()) |
apps/sirloin/cmd/app/main.go:237, :263 |
| flank → brain (HTTP) | Clerk session JWT minted per request; brain enforces ADMIN role | apps/flank/app/lib/brain-http-client.ts, apps/flank/app/lib/auth.ts |
| brain → round (gRPC) | credentials.createInsecure() |
apps/brain/src/modules/application/round/providers/round-grpc-client.provider.ts:19 |
brain → sirloin jobs (gRPC jobs.v1.JobService) |
Shared static token x-service-token (SIRLOIN_BRAIN_SERVICE_TOKEN), warn-and-allow while unset; enqueueable-kind registry limits reachable jobs |
apps/sirloin/internal/app/services/jobs_token.go; apps/brain/src/modules/application/sirloin/adapters/sirloin-jobs.adapter.ts |
| sirloin gRPC server | Plain TCP, no TLS configured at server | apps/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
Section titled “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)
Section titled “Bypass patterns (real, found in code)”These bypasses exist and must be tracked because they alter trust:
- Strip dev mode —
apps/strip/internal/app/middleware/auth.go:79-87readscfg.Stage == env.StageDevelopmentintodevMode. Two distinct bypasses exist:STRIP_AUTH_BYPASSUUID (env-drivencfg.AuthBypassUUID), accepted viaX-Auth-Bypassheader or?auth=query param atauth.go:92-112regardless of stage; setsuserId=test_user,userEmail=test@example.comand logsSECURITY WARNING.- Dev-no-Clerk fallthrough at
auth.go:117-125: whenam.devMode && am.clerkService == nilthe request is admitted asuserId=dev_user. This branch should never trigger in staging or production.
- Internal email bypass on rate limits —
apps/sirloin/internal/pkg/ratelimit/ratelimit.go: any caller whose request proto returns an@foxy.aiemail 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 areapps/brain/src/app.controller.ts:10(GET /hello) and:16(GET /health). No other public routes were found via grep acrossapps/brain/src. - OPTIONS preflight — brain’s Clerk guard returns
truefor anyOPTIONSrequest (apps/brain/src/modules/application/auth/guards/clerk-auth.guard.ts:17-19).
Auth boundaries diagram
Section titled “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
Section titled “Cross-references”- Auth boundaries — short rule statement per service
- Authentication flow — sequence diagrams
- Security model — secrets, transport, webhook signatures
- ADRs touching auth: none under
docs/src/content/docs/decisions/as of 2026-05-05.
Open TODOs
Section titled “Open TODOs”- TODO(@pawel): timing-attack analysis for brain
AUTHORIZED_KEYScomparison and decision oncrypto.timingSafeEqual. - TODO(@law): plan to introduce mTLS or any transport encryption on the internal gRPC mesh — none found in code today.