> ## Documentation Index
> Fetch the complete documentation index at: https://team.k0labs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# JWT Auth

> Replace session-based round-trip authentication for API services with JWT cookies. The auth service will mint JWTs containing user identity, org memberships, and project roles (in a `k0r` claims namespace), set them as HttpOnly cookies, and API services will verify JWTs locally using JWKS -- eliminating the per-request round-trip to the auth service.

<Error>
  This is a temporary and never ever considered production-ready architecture. It was something we built and for the sake of a demo kept in place while we carefully designed and architected the KeyCloak deployment.
</Error>

This document outlines the proposed architecture for replacing session-based round-trip authentication with JWT cookies in the k0rdent platform. The goal is to eliminate the per-request round-trip to the auth service, reducing latency and improving scalability.

***

## Current Architecture (Problem)

Currently, every API request triggers a round-trip from arc-api/atlas-api to the auth service, which queries the database. This adds latency and creates a coupling between API services and the auth service.

```mermaid theme={null}
sequenceDiagram
    participant Browser
    participant NextJS as Next.js Server Action
    participant API as arc-api / atlas-api
    participant Auth as Auth Service
    participant DB as Database

    Browser->>NextJS: Request (session cookie)
    NextJS->>API: Forward cookies + API key
    API->>Auth: GET /v1/auth/session (forward cookies)
    Auth->>DB: Look up session, user, orgs
    DB-->>Auth: Session + user + org data
    Auth-->>API: EnrichedSession JSON
    API-->>NextJS: API response
    NextJS-->>Browser: Page/data
```

Every API request triggers a round-trip from arc-api/atlas-api to the auth service, which queries the database. This adds latency and creates a coupling between API services and the auth service.

## Target Architecture

```mermaid theme={null}
sequenceDiagram
    participant Browser
    participant NextJS as Next.js Server Action
    participant API as arc-api / atlas-api
    participant Auth as Auth Service
    participant DB as Database

    Note over Browser,Auth: Login / Session Refresh
    Browser->>Auth: Sign in
    Auth->>DB: Create session, fetch orgs+projects
    Auth-->>Browser: Session cookie + JWT cookie (k0r-jwt)

    Note over Browser,API: Subsequent API Calls
    Browser->>NextJS: Request (session cookie + JWT cookie)
    NextJS->>API: Forward cookies + API key
    API->>API: Verify JWT locally (JWKS cached)
    API-->>NextJS: API response
    NextJS-->>Browser: Page/data
```

## JWT Payload Structure

The JWT uses a `k0r` namespace for custom claims, following the structure you specified:

```json theme={null}
{
  "iss": "<AUTH_BASE_URL>",
  "sub": "<user.id>",
  "aud": "k0rdent-api",
  "exp": 1719500000,
  "iat": 1719496400,
  "name": "Jerry Smith",
  "email": "jerry@acme.com",
  "email_verified": true,
  "k0r": {
    "principal_type": "employee",
    "orgs": {
      "org-abc123": ["owner"],
      "org-def456": ["member"]
    },
    "projects": {
      "proj-789": { "org": "org-abc123", "roles": ["member"] },
      "proj-012": { "org": "org-abc123", "roles": ["admin"] }
    }
  }
}
```

## Changes Required

### 1. Database: Add JWKS Table

The Better Auth JWT plugin requires a `jwks` table to store key pairs. Add it to the Drizzle schema for type safety, then run the Better Auth migration.

**File**: [packages/db/src/schema/auth.ts](packages/db/src/schema/auth.ts)

* Add a `jwks` table definition (id, publicKey, privateKey, createdAt, expiresAt)
* Register in the drizzle adapter schema in `auth.ts`

**Migration**: Run `pnpm dlx @better-auth/cli migrate` from the auth service directory.

### 2. Auth Service: Configure JWT Plugin with Rich Payload

**File**: [services/auth/src/auth.ts](services/auth/src/auth.ts)

Update the `jwt()` plugin configuration:

```typescript theme={null}
jwt({
  jwt: {
    issuer: process.env.AUTH_BASE_URL ?? 'http://localhost:8000',
    audience: 'k0rdent-api',
    expirationTime: '15m',
    definePayload: async ({ user }) => {
      // Query org memberships and project memberships from DB
      // Build the k0r claims namespace
      return { /* rich payload */ };
    }
  }
})
```

The `definePayload` function needs to:

* Query `organizationMembers` joined with `organizations` for the user
* Query `projectMembers` joined with `projects` for the user
* Build the `k0r` namespace with org and project role maps
* Determine `principal_type` from user role

### 3. Auth Service: Set JWT as HttpOnly Cookie

The JWT plugin returns tokens via headers/endpoints but does not set cookies. We need to add a mechanism to set the JWT as a cookie.

**Approach**: Add Hono middleware to the auth service that intercepts responses from session-creating endpoints (sign-in, OAuth callback, session refresh). When a session cookie is being set, also mint a JWT and set it as a `k0r-jwt` cookie.

**File**: [services/auth/src/lib/jwt-cookie.ts](services/auth/src/lib/jwt-cookie.ts) (new)

* Hono middleware that runs after Better Auth processes requests
* Detects when `better-auth.session_token` cookie is set in the response
* Calls `auth.api.getToken()` (internal JWT plugin API) to mint a JWT
* Appends a `Set-Cookie: k0r-jwt=<token>; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=900` header

**File**: [services/auth/src/index.ts](services/auth/src/index.ts)

* Apply the JWT cookie middleware to the Better Auth catch-all handler

**Also handle logout**: When the session is destroyed (DELETE `/session`), clear the `k0r-jwt` cookie by setting it with `Max-Age=0`.

### 4. Auth Service: Custom Session Endpoint Enhancement

**File**: [services/auth/src/routes/v1/session.ts](services/auth/src/routes/v1/session.ts)

* In the `GET /session` handler, after returning the enriched session, also set/refresh the `k0r-jwt` cookie so it stays in sync with session refreshes
* In the `DELETE /session` handler, clear the `k0r-jwt` cookie
* In the `POST /session` (login) handler, the middleware from step 3 handles this

### 5. API Services: JWT Verification Middleware

Create a shared JWT verification utility and update both API services.

**File**: [packages/api/src/jwt.ts](packages/api/src/jwt.ts) (new)

* `createJwtMiddleware(options)` factory function for Hono middleware
* Uses `jose` library (`createRemoteJWKSet`, `jwtVerify`) to verify JWTs
* Reads JWT from `k0r-jwt` cookie
* Caches the JWKS (remote JWKS set handles this automatically)
* Extracts and types the `k0r` claims namespace
* Exports TypeScript types for the JWT payload (`K0rJwtPayload`, `K0rClaims`)

**File**: [apps/arc-api/src/lib/middleware/session.ts](apps/arc-api/src/lib/middleware/session.ts)

* Add JWT verification as the primary auth path
* Extract user, org, and project data from JWT claims
* Fall back to current cookie-forwarding session validation if no JWT cookie (migration period)
* Set context variables (`user`, `session`, `currentOrganization`, `orgRole`) from JWT payload

**File**: [apps/atlas-api/src/lib/middleware.ts](apps/atlas-api/src/lib/middleware.ts)

* Same pattern as arc-api
* Extract `userRole` from JWT claims (`user.role` or `k0r.principal_type`)

**Dependencies**: Add `jose` to `packages/api/package.json` and the catalog.

### 6. Cookie Size Consideration

Standard cookies have a \~4KB limit. A JWT with many org/project memberships could exceed this. Mitigation options:

* **Short-term**: Most users have few orgs/projects; monitor payload size
* **Long-term**: If payload grows too large, store a JWT reference in the cookie and look up claims from a cache, or split into essential claims (current org only) + a separate endpoint for full claims

For now, the implementation will include a size check and log a warning if the JWT cookie approaches 4KB.

### 7. Next.js Apps: Forward JWT Cookie

No changes needed for the Next.js Server Actions -- they already forward all cookies (including `k0r-jwt`) via `getCookieHeader()` in [apps/arc/src/lib/auth.utils.ts](apps/arc/src/lib/auth.utils.ts). The JWT cookie will be automatically forwarded alongside the session cookie.

The auth client plugins (`jwtClient()`) are optional and only needed if the frontend wants to explicitly request JWT tokens. Since the JWT is set as a cookie by the auth service, no client-side plugin changes are required.

## Key Design Decisions

* **JWT as cookie (not Bearer)**: Cookies are automatically forwarded by the browser and Next.js Server Actions, requiring zero changes to the API call pattern
* **15-minute JWT expiry**: Short enough to limit stale claims, long enough to avoid constant refresh overhead. Refreshed on every `getSession` call (which happens on page loads via `cookieCache`)
* **Fallback to session validation**: During migration, if no JWT cookie is present, the API services fall back to the current cookie-forwarding approach
* `**jose` for verification\*\*: Standard, well-maintained library for JWT/JWKS operations. Already transitively available (in pnpm-lock.yaml via better-auth)
* **JWKS caching**: `createRemoteJWKSet` from `jose` handles caching and key rotation automatically
