Documentation

Auth and authorization

Sessions, JWTs, the internal secret, the super-admin token, and the ownership model.

Source: api.packr.blueforge.studio/-/docs/auth

Authentication & Authorization

This document covers the full auth model for the Packr registry: session/token auth, the two-layer bypass system (internal-secret vs super-admin-token), and the package ownership model.


Auth layers

Packr has three levels of inbound auth:

LayerCredentialWho uses it
Session / JWTBearer token in Authorization header or packr_session cookieHuman users (dashboard, CLI)
Internal secretX-Internal-Secret headerServices calling the registry on behalf of the system (Stripe webhook, dashboard OAuth callback)
Super-admin tokenX-Super-Admin-Token headerHuman emergency override — deceased/locked-out maintainer recovery

Session / JWT

AdminAuthMiddleware accepts two credential types:

  1. Session token (HMAC) — created at OAuth login, stored in dashboard cookie or returned to CLI. Short-lived (7 days).
  2. JWT (device flow) — created by packr-cli login. Valid for 30 days. The JWT carries org_id, user_id, permissions, and scopes.

Both are passed as Authorization: Bearer <token>. On success, the user's internal integer ID is placed in the request context via auth.GetUserID(ctx).

Internal secret (X-Internal-Secret)

Service-to-service calls. The shared secret is set via INTERNAL_API_SECRET env var and configured on both the Go registry and the Next.js dashboard.

Use cases:

  • Dashboard OAuth callback creating a registry session (POST /api/v1/admin/sessions)
  • Stripe webhook updating a subscription (POST /api/v1/admin/subscriptions)
  • Device flow callback authorizing a pending code (POST /api/v1/admin/device/callback)
  • System-managed ownership changes (e.g. an automated pipeline adding a CI user as co-owner)

What it bypasses: The ownership check on add-owner, remove-owner, and transfer endpoints.

What it does NOT bypass: The super-admin-only reset-owners endpoint. Internal secret is for automated systems; it should not hold blanket override authority over package ownership.

Super-admin token (X-Super-Admin-Token)

Human emergency override. The token is set via SUPER_ADMIN_TOKEN env var.

Use cases:

  • A package maintainer is deceased, unreachable, or their account is locked.
  • An abandoned package needs to be transferred to an active maintainer.
  • A CI system accidentally published with the wrong owner; the original dev is unavailable.

What it bypasses: All ownership checks including the reset-owners endpoint. This is intentionally broader than the internal secret.

What it does NOT bypass: Basic request validity (package must exist, new owner list must be non-empty, etc.).

Distinction from X-Internal-Secret:

AspectX-Internal-SecretX-Super-Admin-Token
Intended userAutomated servicesHuman operators
Session requiredNoNo
Bypasses ownershipYes (add/remove/transfer)Yes (add/remove/transfer + reset)
Can access reset-ownersNoYes
RotationOn infrastructure deployOn security incident

Package ownership model

Schema

package_owners(package_id, user_id, added_at) is a many-to-many join table. One package may have many owners; one user may own many packages.

Ownership lifecycle

  1. First publish — the user who publishes becomes the sole owner automatically.
  2. Add co-owner — any existing owner can add further owners via POST .../owners.
  3. Remove co-owner — any existing owner can remove another (but not the last one).
  4. TransferPOST .../transfer removes the caller and adds the target. Atomic.
  5. Force-resetPOST .../owners/reset (super-admin only) atomically replaces the entire owner list. Audit-logs the prior state.

super_admin user flag

In addition to the header-based override, a user account can have super_admin=true in the users table. This grants the same authority as X-Super-Admin-Token to any request authenticated with that user's token or session.

Preferred: env-driven first-boot bootstrap

For a fresh registry — or one whose OAuth providers were misconfigured and the previous admin is locked out — set three env vars at deploy time:

BOOTSTRAP_ADMIN_USER=kmandrup
BOOTSTRAP_ADMIN_PASSWORD='A10-char+Strong!Pass'   # passes validate.Password
BOOTSTRAP_GRANT_SUPER_ADMIN=true

At first boot the server creates the user, promotes it to super_admin, and emits an audit event (user.created with actor system:bootstrap). On every subsequent boot, the user row is left untouched and a one-time WARNING logs that BOOTSTRAP_ADMIN_PASSWORD is still set so the operator remembers to unset it from their secrets store. After the row exists, the env vars are inert.

Partial config is fatal at startup — BOOTSTRAP_ADMIN_USER without BOOTSTRAP_ADMIN_PASSWORD (or vice versa) blocks the boot with a message naming both vars. Username and password go through the same validators as POST /-/v1/login, so a malformed env var fails the boot instead of failing on first login.

Fallback: direct DB access

For an already-deployed registry where bootstrap is too late (the user row already exists, or the operator prefers SQL), set the flag with one statement:

-- SQLite / Postgres
UPDATE users SET super_admin = 1 WHERE name = 'kmandrup';

Or via a future admin API endpoint (not yet exposed in the HTTP layer — use direct DB access when the bootstrap path is unavailable).


Audit logging

All owner mutations emit an audit event:

ActionWhen
package.owner.addedco-owner added
package.owner.removedco-owner removed
package.owners.resetforce-reset (details include prior owner list)
package.transferredownership transferred

View recent audit events:

curl -H "Authorization: Bearer $TOKEN" \
  https://api.packr.blueforge.studio/api/v1/admin/audit

Environment variable reference

VarDescription
JWT_SECRETSigns JWTs from packr-cli login. Required for persistent tokens.
SESSION_SECRETSigns session tokens (dashboard + CLI). Required for persistent sessions.
INTERNAL_API_SECRETService-to-service shared secret. Required for dashboard login.
SUPER_ADMIN_TOKENHuman emergency override. Keep in a password manager.

Generate all secrets with openssl rand -hex 32.