Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 16 additions & 7 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,18 @@ REDIS_URL=redis://localhost:6379

# Auth — NextAuth v5
# AUTH_SECRET also keys the AES-256-GCM encryption of stored SSO client
# secrets (src/server/crypto.ts) — rotating it invalidates them, so re-enter
# providers in Settings → Authentication after a rotation.
# secrets (src/server/crypto.ts) — rotating it invalidates sessions and those
# secrets, so re-enter providers in Identity & sign-in after a rotation.
AUTH_SECRET=change-me-to-32-random-bytes
AUTH_URL=http://localhost:3000
ADMIN_EMAIL=admin@example.local # the instance admin (manages SSO providers)
# Environment-backed bootstrap/break-glass instance administrator. Normal
# local users have durable, individually hashed passwords in Postgres.
ADMIN_EMAIL=admin@example.local
ADMIN_PASSWORD=change-me-bootstrap-password
ADMIN_NAME=Forge Administrator
ADMIN_HANDLE=admin

# OAuth providers are now managed in the UI (Settings → Authentication),
# OAuth providers are now managed in the UI (Identity & sign-in),
# stored in the SsoProvider table. These env vars are OPTIONAL bootstrap
# only: if set and no matching DB row exists yet, a provider row is seeded
# from them on first boot. After that, manage everything in the UI.
Expand All @@ -28,10 +33,11 @@ AUTH_GITHUB_SECRET=
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=

# Outbound workspace invitations. EMAIL_SERVER is the compact SMTP URL form;
# Outbound account setup, password reset, password-change, and workspace
# invitation mail. EMAIL_SERVER is the compact SMTP URL form;
# alternatively set SMTP_HOST/SMTP_PORT/SMTP_SECURE/SMTP_USER/SMTP_PASSWORD,
# or use RESEND_API_KEY. Production invite attempts fail closed when no
# transport is configured.
# account-email attempt fails closed when no transport is configured.
EMAIL_SERVER=smtp://user:pass@smtp.example.com:587
EMAIL_FROM="Forge <noreply@forge.local>"
SMTP_HOST=
Expand Down Expand Up @@ -87,7 +93,9 @@ FORGE_AI_BASE_URL=
FORGE_AI_API_KEY=

# ---- Object storage (S3 / MinIO) ----
# Forge stores attachments in an S3-compatible bucket per workspace.
# Forge stores attachments in an S3-compatible bucket per workspace. Global
# user avatars use S3_GLOBAL_BUCKET (default forge-global) with the same S3
# endpoint and credentials, keeping account media outside tenant buckets.
# In dev we run MinIO via docker/docker-compose.yml:
#
# docker compose -f docker/docker-compose.yml up -d minio
Expand All @@ -105,3 +113,4 @@ S3_ACCESS_KEY=forgeminio
S3_SECRET_KEY=forgeminio-dev-password
S3_REGION=us-east-1
S3_FORCE_PATH_STYLE=true
S3_GLOBAL_BUCKET=forge-global
31 changes: 31 additions & 0 deletions DEVLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,37 @@

> Append-only session log. Read at session start. Update at session end.

## 2026-08-25 — Canonical local and external user identity foundation

- Added a provider-neutral instance authentication policy for local-only,
external-only, and hybrid sign-in, including registration, safe provider
auto-redirect, protected environment-backed break glass, password/reset
policy, and lockout controls.
- Added durable per-user scrypt credentials, single-use hashed account setup
and password-reset tokens, enumeration-safe reset requests, session
revocation generations, and audited invited/active/suspended/deleted account
lifecycle management with last-admin and last-workspace-owner guards.
- Added atomic invite-based local registration: a pending workspace invitation
can create the canonical user, verified email, local credential, and
membership in one transaction, while existing accounts must authenticate
before the invitation can affect their login methods.
- Kept local passwords and linked OIDC/OAuth login identities on one canonical
user while preserving Integration Connections as a separate authorization
boundary. Added self-service login-method management and global S3-backed
profile pictures with provider-image fallback.
- Added workspace-role authorization enforcement for project mutations and
policy primitives for future restricted-project and integration capability
grants. Grant persistence and management UI remain intentionally deferred.
- Kept the database migration additive and compatibility-first: existing users
are normalized, the prior hybrid behavior is seeded, and case-variant email
conflicts fail explicitly instead of silently merging accounts.
- Verification: the migration applied from an empty v0.32-compatible database;
lint and typecheck passed; the serialized local suite passed 1,577 tests with
one intentional skip; focused identity/authorization coverage passed 58
tests; the complete Playwright run passed 59 tests with one scenario-only
skip; and the rebuilt focused identity suite passed all four local login,
enumeration-safe reset, password rotation, and invite-registration flows.

## 2026-07-30 — v0.32.0 release preparation

- Squash-merged AXI-168 implementation PR #94 to `main` at
Expand Down
69 changes: 65 additions & 4 deletions docs/guide/instance-admin.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,71 @@ so they can manage it immediately).

### Users (`/admin/users`)

Every user on the instance, with their instance role, workspace count,
and handle. Promote or demote a user's `instanceRole` inline, and
invite a new user (optionally as an instance admin). This is the only
place instance role is set.
Every canonical user on the instance, including lifecycle status, attached
login methods, instance role, workspace count, and handle. Administrators can:

- create an invited user and email a single-use account-setup link;
- resend setup for an invited user or send password reset for a user who has a
local password;
- promote or demote `instanceRole` and revoke every active session;
- suspend and later reactivate an account; or
- soft-delete and anonymize an account while preserving authored work and
audit attribution.

Suspension immediately invalidates sessions, revokes personal API keys,
invalidates pending account tokens, disconnects user-owned integration
credentials, and pauses their workspace mappings. Deletion additionally
removes local and linked login credentials, memberships, and the global avatar,
and replaces personal fields with a tombstone. Reactivation restores the Forge
principal but does not restore revoked keys or external credential tokens.

Safety guards refuse demotion, suspension, or deletion of the last active
instance administrator. They also refuse suspension or deletion when the user
is the last active owner of any workspace; transfer ownership first. This is
the only place instance role and account lifecycle are administered.

### Identity & sign-in (`/settings/auth`)

Instance administrators own the singleton authentication policy and the
global OIDC, GitHub, and Google provider registry. The modes are:

| Mode | Normal sign-in methods |
| --------------- | --------------------------------------------------------- |
| `LOCAL_ONLY` | Durable Forge passwords |
| `EXTERNAL_ONLY` | Enabled external providers; optional operator break glass |
| `HYBRID` | Durable passwords plus enabled external providers |

The policy also controls registration, optional automatic redirect, password
minimum length, reset expiry, and lockout behavior. Provider/client secrets
remain encrypted with `AUTH_SECRET`; the environment operator remains a
separate recovery credential when break glass is enabled.

All identity-policy and account-lifecycle mutations write the instance-wide
security audit ledger with actor, target, request metadata, and timestamp.

::: warning Authorization scope in this release
The identity core enforces existing workspace roles and closes project mutation
paths that previously treated every membership as equivalent. The schema and
management UI for restricted-project grants and per-integration capabilities
(for example GitHub repo read/link/sync/write) remain follow-up work. A login
identity never implies permission to use an Integration Connection.
:::

## Migration and rollback compatibility

The identity migration is additive. It backfills normalized email keys, creates
the policy/credential/token/avatar/audit tables, adds provider archival state,
and seeds `HYBRID + INVITE_ONLY + break glass` to preserve the
pre-policy presentation. Migration aborts if case-insensitive duplicate user
emails already exist, rather than merging people silently.

Do not drop the new tables as a routine rollback. An older Forge binary can
ignore the additive columns, but it cannot authenticate durable
`LocalCredential` passwords or enforce the new lifecycle/policy state. Before
an application rollback, ensure the environment bootstrap operator and a
working external provider are available; local-only users otherwise cannot
sign in until the new version is restored. Keep the migrated data intact and
roll forward after diagnosis.

### Runtimes (`/admin/runtimes`)

Expand Down
18 changes: 16 additions & 2 deletions docs/guide/local-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,27 @@ Every command prints what it started, skipped, migrated, generated, seeded,
or replaced plus the selected local database and endpoints. The TypeScript
orchestrator works when invoked from Windows PowerShell or Git Bash.

Sign in with the bootstrap credentials it prints:
Sign in with the durable local owner account it prints:

```
owner@forge.local / forge-dev
```

(Override via `ADMIN_EMAIL` / `ADMIN_PASSWORD` env vars before running.)
On an empty local database the seed creates a `LocalCredential` for this user.
Override the owner address with `FORGE_SEED_OWNER_EMAIL` (or
`E2E_OWNER_EMAIL` under E2E) and the seeded password with
`FORGE_SEED_OWNER_PASSWORD`, `E2E_OWNER_PASSWORD`, or `ADMIN_PASSWORD` (in
that precedence order). Existing credentials are never overwritten by a later
seed.
`ADMIN_EMAIL` / `ADMIN_PASSWORD` remain the separate bootstrap and break-glass
operator path, so local development can exercise ordinary password login and
recovery without conflating it with emergency access.

Account setup and password reset send email through the configured local mail
transport. For end-to-end tests, use the fixture-captured delivery/link rather
than a real mailbox. Avatar uploads use the same local MinIO service as
attachments but live in the instance-global `S3_GLOBAL_BUCKET` (default
`forge-global`), not a workspace attachment bucket.

The first visit to a route is slower than later refreshes because Next compiles
that route on demand in development. Forge pins Turbopack to the repository
Expand Down
81 changes: 69 additions & 12 deletions docs/guide/settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,10 @@ interact with the dispatch mode.
Invite, remove, and manage members.

- Invite — send an expiring, single-use email link for `ADMIN`, `MEMBER`, or
`GUEST`. The recipient must authenticate as the invited email before Forge
creates membership; `OWNER` remains transfer-only.
`GUEST`. Existing users authenticate as the invited email. When local
registration is enabled, a new recipient can instead create the canonical
user, verified email, password, and membership atomically from that exact
invitation. `OWNER` remains transfer-only.
- Invitations — pending invites appear before accepted, expired, and revoked
history. Resend rotates the bearer token only after the replacement email is
accepted by the configured provider; revoke disables the pending token.
Expand Down Expand Up @@ -189,13 +191,43 @@ URL: `/settings`. These follow the user across all workspaces.

### Account

- **Email** — the address NextAuth knows you by. Editable; verification
required.
- **Profile picture** — upload one PNG, JPEG, GIF, or WebP image up to 5 MiB.
It is global to your Forge account, not copied into each workspace. Removing
it restores the last linked-provider picture when available.
- **Email** — the case-insensitive canonical address shared by every login
method attached to this account.
- **Handle** — your `@handle`, used in mentions and the activity feed.
- **Name** — display name.
- **Password** — change password (if password auth is enabled in this
deployment).
- **Sessions** — list of active sessions; revoke individually.

### Security & sign-in

URL: `/settings/security`. A Forge account is one canonical `User`; local and
external login methods can be added to or removed from that same account.

- **Local password** — add or change a durable password. Changing it revokes
existing sessions. Removing it requires another linked login method and is
forbidden while the instance is in local-only mode.
- **Linked login methods** — attach enabled OIDC, GitHub, or Google identities,
or unlink one after another login method exists. Linking proves control of
the provider account; unlinking revokes existing Forge sessions.
- **Sessions** — sign out all devices. Password, login-method, lifecycle, and
role changes also increment the account authorization version so existing
JWT sessions stop authorizing.

The sign-in screen exposes **Forgot password** when local credentials are
available. Reset requests are enumeration-safe, rate-limited, expire according
to instance policy, and consume a single-use emailed token. Administrators can
also send a reset for a non-deleted account with a local password from
`/admin/users`; suspension still prevents sign-in until an administrator
reactivates the account.

::: info Login identity is not an integration connection
An Auth.js `Account` row proves who you are when signing into Forge. An
**Integration account** under `/settings/connections` authorizes operations
against GitHub or another external system and may be mapped into workspaces.
Linking or unlinking a GitHub login never creates, changes, or deletes a GitHub
integration connection.
:::

### Appearance

Expand Down Expand Up @@ -224,14 +256,34 @@ See [Automation → API keys](/automation/api-keys.html).

### Authentication

URL: `/settings/auth`. **Instance-admin only** — gated on the operator
whose email matches `ADMIN_EMAIL`, since sign-in providers are global to
the whole self-hosted instance (auth is per _user_, not per workspace).
URL: `/settings/auth`. **Instance-admin only** — gated by
`User.instanceRole === INSTANCE_ADMIN` (with `ADMIN_EMAIL` as the bootstrap
fallback), because sign-in policy and providers apply to the whole instance.

Configure how people sign in, without a redeploy:

- **Email + password** is always available (the bootstrap admin
credential). It can't be removed here.
- **Local only** — present and accept durable Forge passwords. External
providers are not loaded.
- **External only** — present enabled OIDC/OAuth providers. Forge refuses this
mode until at least one external provider is usable. The protected
environment-backed operator may still use `/signin/local` when break glass
is enabled.
- **Hybrid** — present local passwords and any enabled external providers.
- **Automatic redirect** — select one enabled provider to redirect normal
sign-in visits automatically, or leave it unset for the provider chooser.
Manual and error-return visits bypass automatic redirect to avoid loops.
- **Registration** — `DISABLED` requires an administrator-created principal;
`INVITE_ONLY` accepts first-time external sign-in or atomic local account
creation only from an exact invitation; `OPEN` additionally exposes local
email verification/setup and permits eligible external identities to create
their canonical account. Administrator-created local users also activate
through a one-time setup link.
- **Password policy** — configure minimum length, reset-link expiry, failed
attempt threshold, and lockout duration. Passwords are stored as versioned
scrypt hashes; raw passwords and raw reset/setup tokens are never persisted.
- **Break glass** — keeps only the `ADMIN_EMAIL` / `ADMIN_PASSWORD` operator
credential available at `/signin/local`. It does not turn every local user
into a break-glass administrator.
- **Add a provider** — pick a type:
- **OpenID Connect (OIDC)** — the generic, discovery-based type. Covers
any OIDC IdP: self-hosted **Authelia**, Authentik, Keycloak, or hosted
Expand All @@ -249,6 +301,11 @@ Configure how people sign in, without a redeploy:
trust the IdP to assert verified emails.
- **Enable / disable** toggles take effect within ~30s, no restart.

Forge prevents policy/provider changes that would select a missing automatic
redirect target or leave external-only mode without an enabled provider.
Disabling, archiving, or deleting an automatic redirect provider clears that
selection.

Existing `AUTH_GITHUB_*` / `AUTH_GOOGLE_*` env vars (if set) are seeded into
this table once on first boot, then managed here — see
[Reference → Environment](/reference/env.html#auth-nextauth-v5).
Expand Down
Loading
Loading