Skip to content
File

Blob: docs/migrations/git-on-cloudflare.md

Markdown101 lines

Migration: git-on-cloudflare → tessera

Apply this in a separate PR against ~/code/git-on-cloudflare after tessera is live and validated.


Scope: web admin only

git-on-cloudflare has two auth systems. This migration only touches the second.

System What it gates Stays unchanged?
A — git Basic-auth git push over HTTPS via POST /:owner/:repo/git-receive-pack. Token-based machine credentials in AuthDurableObject, PBKDF2-hashed. Yes — entirely as-is. The git push flow continues to use Basic-auth tokens; tessera is irrelevant to it.
B — Web admin UI /:owner/:repo/admin*, /:owner/admin/registry*, /auth/api/users, plus repo-admin operations: refs, head, debug-*, pack/*, purge. No — moves behind tessera OIDC.

There is no D1 user table in git-on-cloudflare today, so there is no per-user data to migrate. Identity becomes "the tessera sub from a verified ID token cookie or bearer."


Code changes

1. New env / secrets

wrangler.jsonc vars:

"TESSERA_OIDC_ISSUER": "https://auth.limic.dev",
"TESSERA_OIDC_CLIENT_ID": "<minted in tessera /admin/clients>",
"OPERATOR_SUB": "<the tessera UUID for the limic operator — visible at /account in tessera>"

wrangler secret put TESSERA_OIDC_CLIENT_SECRET.

AUTH_ADMIN_TOKEN (the existing wrangler secret backing the /auth/api/users Bearer path) is removed at the end of this migration.

2. New requireOidcSession middleware

Create src/auth/oidc.ts:

import { jwtVerify, createRemoteJWKSet } from "jose";

let cachedJwks: ReturnType<typeof createRemoteJWKSet> | null = null;
const getJwks = (env: Env) => {
  if (!cachedJwks) cachedJwks = createRemoteJWKSet(new URL(`${env.TESSERA_OIDC_ISSUER}/api/auth/jwks`));
  return cachedJwks;
};

export const requireOidcSession = async (request: Request, env: Env): Promise<{ sub: string }> => {
  const cookie = request.headers.get("cookie") ?? "";
  const idToken = parseCookie(cookie, "tessera_id_token") ?? bearerToken(request);
  if (!idToken) throw redirectToTesseraSignIn(request, env);

  const { payload } = await jwtVerify(idToken, getJwks(env), { issuer: env.TESSERA_OIDC_ISSUER });
  if (payload.sub !== env.OPERATOR_SUB) throw new Response("forbidden", { status: 403 });
  return { sub: payload.sub };
};

3. Replace the existing admin auth check

Every call site that currently does verifyAuth(env, owner, request, true) for admin routes:

File Line Change
src/routes/ui/adminPage.ts :27 await requireOidcSession(request, env)
src/routes/admin.ts :36-344 (each handler) replace with await requireOidcSession(request, env)
src/routes/auth.ts :30, :50, :84 (the /auth/api/users Bearer-admin handlers) replace getBearerToken + stub.adminAuthorizeOrRateLimit() with await requireOidcSession(request, env)

The git Basic-auth path (src/auth/verify.ts) is unchanged.

4. Add OIDC callback route

The web admin UI needs a place to land after tessera signs the user in. Add:

  • GET /auth/start — redirects to tessera's /api/auth/oauth2/authorize (PKCE).
  • GET /auth/callback — exchanges the code for an ID token, sets a tessera_id_token httpOnly cookie, redirects to the original ?next= parameter.

Sketch is identical to the anvil migration's § 2 — see anvil.md.

5. Remove AUTH_ADMIN_TOKEN

After the cutover:

  • wrangler secret delete AUTH_ADMIN_TOKEN.
  • Remove the adminAuthorizeOrRateLimit path from src/do/auth/authDO.ts:274-323 and the corresponding wrangler secret reference.

What stays unchanged

  • All git Basic-auth routes (System A) and the entire AuthDurableObject (token storage, PBKDF2 hashing, /auth/api/users token-list operation — though the gate moves from Bearer to OIDC).
  • Public read routes (/, /:owner, /:owner/:repo, /tree, /blob, /commits, /commit/:oid).
  • Repo metadata SQLite schema in src/do/repo/db/schema.ts.

No D1 migration

git-on-cloudflare has no users table. There is nothing to add a tessera_sub column to, and nothing to backfill. The first sign-in via tessera is also the first authenticated admin session — there is no historical row to bind.