0024 - Platform-administrator console as a separate trust boundary
Admin console decision separating administrator sessions from tenant console sessions
Esta página aún no está disponible en tu idioma.
- Status: Accepted
- Date: 2026-07-30
- Deciders: nexus core team
- Tags: auth, admin, console, nextjs, bff, security
Context and Problem Statement
Section titled “Context and Problem Statement”The deployment administrator surface (user lifecycle, workspace creation and inspection, workspace membership administration, operator-funded wallet grants) is served by admin-control-plane and requires a credential with deployment-tenant-wide authority.
The tenant console at apps/console/ in the source repository authenticates tenant users against the auth service and holds their access token server-side per 0002 - Next.js App Router for the tenant console.
Reusing that same session for administrator work would mean one cookie, one token audience, and one refresh chain covering both a tenant member and a platform administrator.
A tenant token would then be presentable to admin-control-plane, and an escalation bug in tenant-facing code would reach administrator routes.
Administrator authority also warrants a shorter credential lifetime than a tenant session, and the two lifetimes cannot differ while they share one token type.
Decision Drivers
Section titled “Decision Drivers”- A tenant session must not be usable against the administrator surface, and the reverse, even when both are signed by the same deployment.
- Administrator credential lifetime must be independently configurable and shorter by default.
- Administrator authority must be revocable without waiting for a token to expire.
- Access tokens stay server-side, as they do for the tenant console.
- A rejection on the administrator surface must not disclose whether an email exists, a password was correct, or a subject holds an administrator assignment.
Considered Options
Section titled “Considered Options”- A separate Next.js application with its own cookies and its own token audience — chosen.
- An admin route group inside
apps/consolesharing the tenant session — rejected: one cookie and one audience mean a tenant token is accepted on administrator routes, the two lifetimes cannot diverge, and every tenant-facing authorization bug becomes an administrator-surface bug. - A separate signing key (or asymmetric keys) per audience — rejected: it removes the shared-secret blast radius but adds a second secret to distribute, rotate, and verify across
auth,admin-control-plane, and the administrator console. The audience split plus the per-calladminslookup carries the authorization boundary; the key split only narrows the consequence of an already-total secret compromise. - A separate identity store for administrators — rejected: administrators are existing users with an assignment row, so a second store would duplicate password hashing, account state, and audit plumbing.
Decision Outcome
Section titled “Decision Outcome”Chosen option: a separate administrator console application with a distinct administrator session in the auth service.
The administrator console lives at apps/admin-console/ in the source repository (package @nexus/admin-console, port 14432) and follows the same server-side BFF pattern as the tenant console: the browser never receives a token.
Its routes are POST /api/session/sign-in, POST /api/session/refresh, POST /api/session/sign-out, GET /api/session/me, GET /api/platform/readiness, and GET /healthz.
Protected surfaces sit under an (admin) route group whose server layout calls getCurrentAdmin() and redirects to /sign-in when no valid administrator session is present.
Session cookies are nexus_admin_access and nexus_admin_refresh — names distinct from the tenant console’s, since a port does not scope a cookie — both httpOnly and sameSite=strict, with secure gated by NEXUS_SECURE_COOKIES, expiring at the token’s own expiry.
The sign-in form renders one generic failure message for every rejection.
Three separations distinguish an administrator session from a tenant session in the services/auth/ source:
- Audience.
JwtCodecholds a tenant audience (nexus-console) and an administrator audience (nexus-admin-console), both HS256 overAUTH_JWT_SECRET.verify()accepts only the tenant audience andverify_admin()only the administrator audience, and both claim structs carry#[serde(deny_unknown_fields)]. A tenant access token therefore failsVerifyAdminToken, and an administrator token failsVerifyToken. - Session rows. The
0004_session_kind.sqlmigration adds a CHECK-constrainedsessions.kind('user'|'admin') plus the indexsessions_refresh_lookup_idx (tenant_id, refresh_hash, kind). Refresh rotation and revocation match onkind, so a refresh token minted for one credential cannot be redeemed on the other’s path. - Lifetime. Administrator access tokens default to 30 minutes and refresh tokens to 12 hours, set by
AUTH_ADMIN_ACCESS_TTL_SECSandAUTH_ADMIN_REFRESH_TTL_SECS. Tenant sessions remain 1 day and 30 days. Both session kinds expire after one hour without an authenticated request by default (AUTH_SESSION_IDLE_TIMEOUT_SECS); setting it to0disables inactivity expiry.
The administrator REST surface is POST /api/auth/admin/sign-in, /api/auth/admin/refresh, /api/auth/admin/sign-out, and /api/auth/admin/verify-token.
Sign-in authenticates the password and then requires an active row in the admins table for the deployment tenant; every failure returns 401 with no distinguishing detail.
admin_refresh re-checks the administrator assignment and the user’s active state on every rotation, and VerifyAdminToken re-checks the admins table on every call, so revoking an assignment or disabling an account ends access at the next verification rather than at token expiry.
nexus-admin in admin-cli/ uses the same routes, refreshing through /api/auth/admin/refresh when its stored access token has expired but its refresh token has not.
Consequences
Section titled “Consequences”- A stolen tenant-console token is inert against the administrator surface and a stolen administrator token is inert against tenant routes: two independent verification paths, two cookie pairs, two refresh chains.
- A compromised
AUTH_JWT_SECRETcan mint either audience. The audience split is an authorization boundary between two honest verifiers, not a defense against holding the signing key. This is the accepted cost of one shared secret across both audiences; treatAUTH_JWT_SECRETdisclosure as full compromise of both surfaces and rotate it. - Administrator assignment has no in-product provisioning path. No code path in this repository writes a row into the
adminstable. Creating the first platform administrator requires a direct SQL insert intonexus_auth.admins, and the administrator console neither grants nor revokes assignments. - Administrators re-authenticate after one hour without an authenticated request by default; their refresh tokens remain valid for 12 hours after issuance.
- The image set gains a second Node service alongside
console, and the pnpm workspace gains a second Next.js application to build, lint, and keep on a compatible dependency graph. - The administrator surface is now reachable two ways with the same credential: this console and
nexus-admin. Both authenticate through the same routes, so an authorization change inauthapplies to both without per-client work. - Administrator session lifecycle is auditable on its own event names:
admin.session.signed_in,admin.session.refreshed, andadmin.session.denied(recorded when a correct password belongs to a subject holding no administrator assignment). No token material is recorded.
More Information
Section titled “More Information”- Tenant console BFF pattern: 0002 - Next.js App Router for the tenant console
- Administrator surface and trust boundaries: Threat Model
- Service inventory: architecture services documentation in the source repository
