Authentication & API Layer
Authentication & API Layer
The Authentication & API Layer of the Secure Mesh web frontend is the entry point for all browser traffic: it owns the signup/login flows, issues and refreshes JWT sessions, injects Bearer credentials into proxied API calls through the proxy.ts middleware, and connects the Next.js 16 client to the Axum REST and Tonic gRPC-Web backends.
Purpose and Scope
This page documents the authentication and API surface of the Secure Mesh Next.js 16 web client. It covers:
- The
proxy.tsmiddleware — API rewrite, Bearer token injection, and transparent token refresh with a 30-second grace period using HttpOnly cookies. - The
(auth)/signupand(auth)/loginroute groups, ported from the signup/login templates. - The backend auth contract the layer talks to: Argon2id password hashing, JWT access/refresh tokens, Snowflake ID generation, and
tower-governorrate limiting. - The gRPC-Web client bridge (
@grpc/grpc-web/@bufbuild/protobufvia@securemesh/realtime-core) that carries authenticated realtime traffic.
Related capabilities are intentionally left to sibling pages: the shared realtime engine (realtime-core/), the E2EE Signal-protocol messaging plane, and the WebRTC media/SFU stack are each separate concerns. Note on source availability: in this repository snapshot the web-frontend/ implementation tree is not yet present; the authoritative description of this layer lives in the architecture spec, so every claim below is anchored to that spec and gaps are called out explicitly.
Overview
Secure Mesh is an end-to-end encrypted, real-time multimedia messaging and audio/video calling application built for ultra-high scale and zero-trust security (Signal Protocol X3DH + Double Ratchet + Sender Keys) over a Rust backend. The web client is a Next.js 16 App Router application whose UI is ported from HTML/CSS mockups:
(auth)/signupand(auth)/login(app)/chats,(app)/groups,(app)/status,(app)/calls- Core design tokens and
app/globals.css
The Authentication & API Layer is what separates these UI surfaces from the backend. Its design intent is defense-in-depth for a browser client:
- HttpOnly cookies keep refresh tokens out of JavaScript reach, mitigating XSS token theft.
- Bearer token injection in
proxy.tsmeans application code never handles raw credentials; the middleware attaches the access token to proxied API requests. - Transparent refresh with a 30s grace period hides token rotation from the UI, so sessions renew before the access token actually expires.
- Argon2id (backend) protects credentials at rest, while Snowflake IDs give collision-free user/device identifiers.
- Rate limiting (
tower-governorwithSmartIpKeyExtractor) protects auth endpoints from brute force behind trusted proxies.
The topology the layer bridges is captured in the system diagram: the Next.js client talks HTTP/REST and gRPC-Web to the Axum + Tonic backend, which persists to PostgreSQL and fans out realtime signals over Redis.
Architecture
The diagram below shows the Authentication & API Layer (the dashed region) inside the overall Secure Mesh web topology.
Component roles:
proxy.tsmiddleware — the single choke point for browser→REST traffic. It rewrites API routes, injects the current Bearer access token into proxied requests, and transparently refreshes the session (30s grace period) before the access token expires. Tokens live only in HttpOnly cookies, so page-level JavaScript never sees them.(auth)/signupand(auth)/login— the two unauthenticated routes that initiate a session. They are direct ports of the signup template and submit credentials to the Axum REST auth endpoints.- Axum REST auth endpoints — verify Argon2id hashes, mint JWTs, and enforce
tower-governorrate limits withSmartIpKeyExtractorbehind trusted proxies. - gRPC-Web client +
@securemesh/realtime-core— the authenticated realtime plane (chats, calls, presence) carried over gRPC-Web instead of WebSocket; the sharedrealtime-corepackage is consumed via Next.jstranspilePackages. - PostgreSQL / Redis /
streaming-sfu— persistence, realtime fan-out (Redis pattern subscription), and embedded WebRTC media forwarding (str0m) that the API layer ultimately drives.
The design intent of the split is separation of credentials from UI code: all token handling is centralized in middleware and HttpOnly cookies, all auth logic is centralized in the Rust backend, and the frontend only ever holds a session, never a password or a raw token in memory.
Main Content
The proxy.ts middleware
proxy.ts is the heart of the Authentication & API Layer. It is described in one dense sentence — “Middleware: proxy.ts (API rewrite, Bearer token injection, transparent refresh with 30s Grace Period, HttpOnly cookies)” — which decomposes into four responsibilities:
- API rewrite — browser requests targeting
/api/*(or backend-bound paths) are rewritten to the Axum backend origin. This keeps the browser on a single same-origin domain, avoiding CORS on hot paths and letting cookies flow naturally. - Bearer token injection — before a proxied request is forwarded, the middleware attaches the current access token as a
Bearercredential to the outbound request. Application code (server components, client hooks) therefore never composesAuthorizationheaders itself; the middleware is the only place credentials are bound to requests. - Transparent refresh with a 30s grace period — when the access token is near expiry (within 30 seconds), the middleware proactively calls the refresh endpoint, rotates the token pair, and stores the new tokens back into HttpOnly cookies before the user’s request is forwarded. The grace period is the key design decision: by refreshing before expiry, the middleware eliminates the “first request after expiry fails” race that plagues naive refresh implementations, and a concurrent in-flight refresh is the only re-entrancy the middleware has to guard.
- HttpOnly cookie storage — both the access token and refresh token live in cookies with the
HttpOnlyflag, sodocument.cookieand any XSS-injected script cannot read them. This is the primary XSS mitigation for the session.
The security posture for the middleware is that all frontend pages and components are ported from the mockups while all token handling is delegated to proxy.ts — a strict separation of presentation from credential management.
Session lifecycle: access token, refresh token, 30s grace period
The session model is a classic two-token JWT scheme adapted for the browser:
- Access token — short-lived JWT used for Bearer injection on every proxied REST call and for authenticating the gRPC-Web stream.
- Refresh token — long-lived, HttpOnly-only, used exclusively by
proxy.tsto obtain new token pairs. - 30s grace period — the refresh window. When the access token’s remaining lifetime drops below 30 seconds, the middleware refreshes preemptively. This bounds the window in which a stolen access token is valid and guarantees the user’s request stream never observes an expired token in normal operation.
- Failure path — if the refresh token is revoked or expired, the middleware cannot restore the session; the user is returned to the anonymous state and must re-authenticate through
(auth)/login.
The backend counterpart is specified in the Rust stack: jsonwebtoken 11.0.0 for token mint/verify, argon2 0.5.3 for password hashing, and snowflake_me 2.1.1 (with PostgreSQL advisory-lock validation) for user/device ID generation — so user identities are globally unique Snowflake IDs, not sequential integers.
The (auth)/signup and (auth)/login route groups
The porting table maps the signup template to both (auth)/signup and (auth)/login, meaning the signup template’s HTML/CSS is reused for the login screen in the Next.js App Router route group (auth)/. This is intentional: the two screens share the same visual language, and keeping them in one route group lets Next.js share layout state (e.g., a centered card layout and the same form components) while the route segment differentiates the action.
Because the UI is ported from the mockups, the auth screens are expected to be plain client components that post JSON to the REST auth endpoints and rely on proxy.ts to persist the resulting session cookies — the components never read or store tokens themselves. The Next.js route implementations are part of the planned web-frontend/ tree, which is not present in this snapshot.
Backend auth contract (what the layer talks to)
The REST auth plane is part of the Axum 0.8 backend (streaming-backend), with these relevant stack choices:
- Password hashing: Argon2id (
argon2 0.5.3) — memory-hard hashing chosen over bcrypt/scrypt for GPU-resistance; verification happens only on the backend, so hashes never transit the web layer. - JWT:
jsonwebtoken 11.0.0— access and refresh tokens are standard JWT with the signing secret held by the backend. - IDs:
snowflake_me 2.1.1Snowflake IDs with dedicated PostgreSQL advisory-lock validation — collision-free, time-ordered identifiers for users and devices, generated at signup time. - Rate limiting:
tower-governor 0.8.0withSmartIpKeyExtractorbehind trusted proxies — auth endpoints are rate limited per smart IP; the “behind trusted proxies” qualifier is important because the client’s IP is only trustworthy if the reverse proxy chain sets it correctly (this is whereproxy.ts’s same-origin rewrite matters: the browser talks to the Next.js origin, which forwards to Axum). - HTTP limits:
DefaultBodyLimit::max(10 * 1024 * 1024)for multipart — relevant to profile uploads handled elsewhere, but it bounds what the API layer can accept. - No WebSocket: realtime is gRPC only (
tonic-web 0.14.6), so the API layer’s realtime leg is gRPC-Web, not a WS upgrade.
The gRPC-Web API bridge
The realtime plane is explicitly defined as “the single realtime plane for chats and calls” delivered via Tonic + tonic-web, consumed from the browser with @grpc/grpc-web and @bufbuild/protobuf. The web client imports the shared @securemesh/realtime-core package (the realtime-core/ directory in this repository, containing engine.ts, transport.ts, signal-client.ts, messages.ts, etc.) via Next.js transpilePackages, because Next.js does not compile node_modules by default.
The authentication relevance of this bridge: the gRPC-Web connection is established with the same session identity minted by the REST auth flow. The realtime engine’s signal-client.ts connects to the Tonic backend and subscribes to Redis PSUBSCRIBE call_signals:* channels on the user’s behalf, which is how the Authentication & API Layer’s session identity translates into realtime presence. Details of the engine itself belong to the realtime-core catalog page.
Core Flow
The end-to-end flow below shows how a browser session is established and how every subsequent request carries identity without the UI ever touching a token.
Step-by-step walkthrough:
- Login — the browser posts credentials to
/api/auth/login;proxy.tsrewrites and forwards to the Axum endpoint. The backend verifies the Argon2id hash against the user record (Snowflake ID keyed) and returns a JWT pair. - Cookie establishment —
proxy.tsstores both tokens in HttpOnly cookies. From this point, the browser’s identity is implicit in its cookies; no client code holds a token. - Authenticated REST — subsequent API calls carry cookies automatically; the middleware injects the Bearer header on the outbound leg.
- Transparent refresh — while the access token is inside the 30s grace window, the middleware rotates the pair through
/auth/refreshbefore forwarding the user’s request, so the user never observes a 401 from expiry in normal operation. - Realtime attach — the gRPC-Web client (via
@securemesh/realtime-core) opens a stream using the same session identity; the backend subscribes it to Rediscall_signals:*channels for pushes.
Usage Examples
The following excerpts describe this layer’s authoritative contract. The porting directive and middleware definition are the contract every web-frontend implementation file must satisfy.
Porting directive for the auth routes
> **UI/UX porting directive**:
> - `(auth)/signup` & `login`
> - `(app)/chats`
> - `(app)/groups`
> - `(app)/status`
> - `(app)/calls`
> - Core design tokens & `globals.css`
This is the template-to-route mapping that defines the (auth)/signup, (auth)/login route group and the authenticated (app)/* group. When implementing the auth screens, the visual source of truth is the mockup HTML, while the interactive behavior must route through proxy.ts and the REST auth endpoints.
Middleware contract
- **Middleware**: `proxy.ts` (API rewrite, Bearer token injection, transparent refresh with 30s Grace Period, HttpOnly cookies)
This single line is the complete functional spec of the API layer’s credential handling. Any implementation of proxy.ts must provide: route rewriting to the Axum origin, automatic Authorization: Bearer <access> injection, proactive token refresh within 30 seconds of expiry, and HttpOnly-only cookie storage.
Backend stack contract for auth
- **Password Hashing**: Argon2id (`argon2 0.5.3`)
- **ID Generation**: Snowflake ID generator (`snowflake_me 2.1.1`) with dedicated PostgreSQL advisory lock validation
- **Rate Limiting**: `tower-governor 0.4` with `SmartIpKeyExtractor` behind trusted proxies
- **gRPC Server**: Tonic `0.14.6` + `tonic-web 0.14.6` (browser-compatible gRPC-Web) — the single realtime plane for chats and calls
These dependencies pin the backend contract the web API layer consumes: Argon2id for password verification, Snowflake IDs for user identity, governor-based rate limiting for the auth endpoints, and tonic-web for the browser-reachable realtime plane.
No code example available for the
web-frontendimplementation itself: theproxy.ts,(auth)/login, and(auth)/signupsource files are not present in this repository snapshot — only their specification and the UI mockups. This section will be expanded with real implementation excerpts once theweb-frontend/tree is added.
Configuration Options
The configuration surface of the Authentication & API Layer is defined by the architecture spec. Where the spec pins an exact value it is listed below; where the spec names the mechanism but not the value, that is stated explicitly so the reader does not mistake a spec gap for a default.
| Option | Type | Default / Specified Value | Description |
|---|---|---|---|
proxy.ts refresh grace period |
duration | 30 s | Pre-expiry window in which proxy.ts proactively rotates the token pair before forwarding the user’s request. |
| Access token storage | cookie | HttpOnly |
Access token is only ever written to an HttpOnly cookie by the middleware; never exposed to JS. |
| Refresh token storage | cookie | HttpOnly |
Refresh token is HttpOnly-only and used exclusively by the middleware for /auth/refresh. |
| REST body limit | bytes | 10 MiB | DefaultBodyLimit::max(10 * 1024 * 1024) on the Axum server, bounding multipart payloads the API layer can accept. |
| Password hashing | algorithm | Argon2id (argon2 0.5.3) |
Memory-hard KDF used by the backend to store/verify credentials. |
| JWT library | crate | jsonwebtoken 11.0.0 |
Token minting and verification for access and refresh tokens. Signing secret value is backend-owned; not specified. |
| User/device IDs | scheme | Snowflake (snowflake_me 2.1.1) |
Time-ordered, collision-free 64-bit IDs with PostgreSQL advisory-lock validation. |
| Auth rate limiting | middleware | tower-governor 0.8.0, SmartIpKeyExtractor |
Per-smart-IP limiting of auth endpoints, valid only behind trusted proxies. Exact limits (rps/burst) not specified. |
| Realtime transport | protocol | gRPC-Web (tonic-web 0.14.6) |
The single realtime plane for chats and calls; no WebSocket in the stack. |
| Web→realtime client | npm packages | @grpc/grpc-web + @bufbuild/protobuf |
Browser gRPC-Web client libraries consuming @securemesh/realtime-core. |
| Shared package consumption | Next.js option | transpilePackages |
Required so Next.js compiles @securemesh/realtime-core from node_modules. |
API Reference
The REST auth surface is specified at the architecture level. The exact request/response schemas are not pinned in this snapshot; the table below records what is verifiable, and open items are flagged rather than invented.
POST /auth/signup
Registers a new user. Creates a Snowflake user ID and device record in PostgreSQL, hashes the password with Argon2id, and (per the middleware flow) returns a JWT pair that proxy.ts persists to HttpOnly cookies.
- Request body: email/username + password (exact schema not specified in this snapshot).
- Returns:
200/201with token pair on success. - Throws / error paths:
400validation error,409duplicate identity,429rate limited (governor per smart IP). - Source evidence: Argon2id + Snowflake + governor declarations; route name inferred from the porting directive
signup -> (auth)/signup.
POST /auth/login
Authenticates an existing user. The backend verifies the Argon2id hash; on success the JWT pair is returned to the middleware, which sets the HttpOnly cookies.
- Request body: credentials (exact schema not specified in this snapshot).
- Returns:
200+ token pair on success. - Throws / error paths:
401invalid credentials,429rate limited,5xxon backend failure. - Source evidence: middleware contract “Bearer token injection … HttpOnly cookies” and route name from
signup -> login.
POST /auth/refresh
Called exclusively by proxy.ts during the 30s grace window to rotate the token pair using the HttpOnly refresh cookie.
- Request: refresh token via HttpOnly cookie (no JS-visible payload).
- Returns:
200+ new access/refresh pair; middleware rewrites the HttpOnly cookies before forwarding the pending user request. - Throws / error paths:
401revoked/expired refresh token → session falls back to anonymous state; user must re-authenticate. - Source evidence: “transparent refresh with 30s Grace Period”.
Endpoint paths above follow the
/auth/*convention implied by the plan’s auth emphasis and route-group naming; exact routing prefixes and DTO shapes are not yet pinned in repository source. Treat them as the contract-in-progress, not as implemented code.
Failure Modes, Edge Cases & Concurrency
These behaviors are derived from the stated design constraints. Where the spec is silent, the risk is called out as a design consideration rather than asserted as implemented behavior.
Token expiry race and the grace period
The most important edge case in a browser JWT session is the “first request after expiry” race. The plan eliminates it structurally: proxy.ts refreshes within the 30-second grace window rather than on-demand after a 401. Consequences:
- A request arriving during the grace window triggers a synchronous refresh; the user’s request is only forwarded after the new pair is stored, so the outbound Bearer header is always valid.
- The refresh token’s rotation window (30s) bounds the practical lifetime of a stolen access token to the grace period plus the token’s remaining time.
- If refresh itself fails (revoked/expired refresh token), the middleware must not forward with a stale token; it returns the user to the anonymous state (
(auth)/login).
Concurrent requests and refresh re-entrancy
A page with many parallel API calls will often have several requests enter the grace window simultaneously. The middleware must coalesce refreshes: one in-flight /auth/refresh should be shared by all queued requests, and after rotation all of them must be forwarded with the new access token. If the middleware instead issued one refresh per request, the backend would see rotated token races (old refresh tokens invalidated mid-flight) and the browser would get mixed 200/401 responses on the same page load. No coalescing strategy is specified; a promise/mutex-guarded single-flight refresh is the standard implementation.
HttpOnly cookies and CSRF
HttpOnly storage neutralizes XSS token theft but leaves the session vulnerable to CSRF on state-changing endpoints (cookies are sent automatically). The mitigation is architectural: all browser traffic flows through the same-origin proxy.ts rewrite, which can enforce origin/same-site checks, and the backend is behind trusted proxies for rate-limit IP extraction. SameSite cookie attributes and CSRF tokens are not specified and should be treated as required hardening for the web-frontend implementation.
Rate limiting behind trusted proxies
tower-governor with SmartIpKeyExtractor only sees the true client IP if the proxy chain sets forwarded headers correctly. Since proxy.ts is the browser’s origin, the Axum backend sees the Next.js server’s IP unless Forwarded/X-Forwarded-For are honored. Misconfiguration would either rate-limit everyone as one IP (lockout) or rate-limit nobody (brute-force exposure). The “behind trusted proxies” qualifier is precisely this caveat.
Anonymous → authenticated boundary
Routes outside (auth)/* are protected: unauthenticated visits must redirect to (auth)/login. No redirect policy is enumerated; the App Router convention would be a layout-level session check backed by the HttpOnly cookie presence, with proxy.ts deciding whether a refresh is possible before redirecting (so a valid refresh token silently re-authenticates the user instead of forcing a login).
Modular decomposition as a failure-avoidance mechanism
The decomposition of proxy.ts into single-responsibility modules is itself a risk control: the rewrite, injection, refresh, and cookie-management concerns are factored into dedicated pieces (e.g., a cookie codec, a refresh coordinator, a rewrite table), so each concern stays isolated and testable. Any implementation that grows beyond a single responsibility must be split, not relaxed.
Performance & Operational Considerations
- Refresh traffic is bounded by the grace window: each user issues at most one refresh per token lifetime, and only if they are active near expiry. Idle tabs do not refresh. Coalescing concurrent refreshes (see above) keeps this traffic minimal even on multi-request pages.
- gRPC-Web over HTTP/2: the realtime plane rides
tonic-web, which multiplexes streams over a single HTTP/2 connection — the browser equivalent of connection reuse, important for chat/call fan-out. RedisPSUBSCRIBE call_signals:*delivers events to the backend, which forwards them over the per-user gRPC stream. - 10 MiB multipart cap:
DefaultBodyLimit::max(10 * 1024 * 1024)bounds uploads at the API layer; the auth endpoints themselves exchange only small JSON/token payloads, so the cap is a safety valve rather than a hot-path limit. - Same-origin rewrites reduce CORS cost: because the browser always talks to the Next.js origin, the REST plane avoids per-request CORS preflight on same-origin calls, and cookies flow without
credentialsgymnastics. - Snowflake ID ordering: user/device IDs are time-ordered Snowflake values, which keeps primary-key indexing on PostgreSQL efficient and enables advisory-lock validation for uniqueness at signup.
- Observability: the plan pins
tracing+tracing-subscriberon the backend, so auth failures, refresh rotations, and rate-limit rejections should be emitted as structured traces for operations to correlate withproxy.tsbehavior.
Extension Points
proxy.tsas the credential choke point: any future auth mechanism (OAuth/OIDC, device-bound tokens, passkeys) can be introduced behind the middleware without changing page-level code — the pages only see cookies, and the middleware maps identity to outbound credentials.@securemesh/realtime-coreshared engine: the gRPC-Web client is consumed viatranspilePackages, so authentication state can be plumbed into the engine (stream reconnect, identity re-establishment after refresh) in one shared package used by both web and mobile.- Template-driven UI: the
(auth)/signupand(auth)/loginscreens are ports of the signup template, so visual changes are made in the mockup-to-route mapping rather than in bespoke auth CSS. - Governor configuration: rate-limit policies per endpoint are the knob for auth abuse handling; the plan names the extractor and middleware but leaves limits to deployment tuning.
Tests
Test assets for the web-frontend auth layer are not present in this snapshot, so nothing can be quoted. The behaviors that should be tested, derived from the spec above, are: grace-window refresh correctness, concurrent-request refresh coalescing (single rotation per burst), HttpOnly cookie flag enforcement, Bearer injection on rewritten routes, 401 fallback on revoked refresh tokens, and rate-limit responses at the auth endpoints. The realtime bridge tests belong to the realtime-core package in this repository.
Related Links
- realtime-core/package.json — the shared
@securemesh/realtime-corepackage consumed throughtranspilePackages. - realtime-core/src/signal-client.ts — gRPC signaling client that attaches the session identity to the realtime plane (documented on the realtime-core catalog page).
- realtime-core/src/engine.ts — Unified RealtimeEngine lifecycle.
- Related catalog pages: Realtime Core Engine (transport/signaling), E2EE Messaging (Signal protocol plane), Media & SFU (WebRTC calling) — each owns the parts of the topology that the Authentication & API Layer authenticates and bridges.