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:
| Layer | Credential | Who uses it |
|---|---|---|
| Session / JWT | Bearer token in Authorization header or packr_session cookie | Human users (dashboard, CLI) |
| Internal secret | X-Internal-Secret header | Services calling the registry on behalf of the system (Stripe webhook, dashboard OAuth callback) |
| Super-admin token | X-Super-Admin-Token header | Human emergency override — deceased/locked-out maintainer recovery |
Session / JWT
AdminAuthMiddleware accepts two credential types:
- Session token (HMAC) — created at OAuth login, stored in dashboard cookie or returned to CLI. Short-lived (7 days).
- JWT (device flow) — created by
packr-cli login. Valid for 30 days. The JWT carriesorg_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:
| Aspect | X-Internal-Secret | X-Super-Admin-Token |
|---|---|---|
| Intended user | Automated services | Human operators |
| Session required | No | No |
| Bypasses ownership | Yes (add/remove/transfer) | Yes (add/remove/transfer + reset) |
Can access reset-owners | No | Yes |
| Rotation | On infrastructure deploy | On 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
- First publish — the user who publishes becomes the sole owner automatically.
- Add co-owner — any existing owner can add further owners via
POST .../owners. - Remove co-owner — any existing owner can remove another (but not the last one).
- Transfer —
POST .../transferremoves the caller and adds the target. Atomic. - Force-reset —
POST .../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:
| Action | When |
|---|---|
package.owner.added | co-owner added |
package.owner.removed | co-owner removed |
package.owners.reset | force-reset (details include prior owner list) |
package.transferred | ownership transferred |
View recent audit events:
curl -H "Authorization: Bearer $TOKEN" \
https://api.packr.blueforge.studio/api/v1/admin/audit
Environment variable reference
| Var | Description |
|---|---|
JWT_SECRET | Signs JWTs from packr-cli login. Required for persistent tokens. |
SESSION_SECRET | Signs session tokens (dashboard + CLI). Required for persistent sessions. |
INTERNAL_API_SECRET | Service-to-service shared secret. Required for dashboard login. |
SUPER_ADMIN_TOKEN | Human emergency override. Keep in a password manager. |
Generate all secrets with openssl rand -hex 32.