diff --git a/apps/web/content/docs/modules/authentication.mdx b/apps/web/content/docs/modules/authentication.mdx index bb17a4b3..9be00635 100644 --- a/apps/web/content/docs/modules/authentication.mdx +++ b/apps/web/content/docs/modules/authentication.mdx @@ -1,10 +1,16 @@ --- title: Authentication -description: Users, local/OAuth login, tokens, teams, and 2FA. -agent_summary: "Client API: /authentication/local/new, /authentication/local, renew, logout, teams; server-side token vault; MCP ?modules=authentication." +description: Users, local/OAuth login, tokens, teams, 2FA, and sudo for sensitive operations. +agent_summary: "Client API: /authentication/local, renew, logout, teams, twoFa; server-side token vault; sudo after fresh login; Admin API cdt_ tokens for automation — not service accounts." --- -The **authentication** module handles user accounts, token issuance, OAuth providers, teams, magic links, and 2FA. Application runtime code uses the **Client API** with user bearer tokens; provisioning uses Admin API or MCP. +import AuthenticationDeepDive from "@/mdx/deep-dives/authentication-deep-dive.mdx"; + +Apps need sign-up, login, token lifecycle, and optional multi-tenant membership without building auth from scratch. The **authentication** module handles user accounts, JWT access/refresh tokens, OAuth providers, teams, magic links, and 2FA. Application runtime code uses the **Client API** with user bearer tokens; provisioning uses the Admin API or MCP. + + + Machine-to-machine and operator automation use **[Admin API tokens](/docs/reference/admin-api)** (`cdt_` prefix) — not Client API service accounts. See [Client vs Admin API](/docs/learn/client-vs-admin-api). + ## Use cases @@ -16,15 +22,19 @@ The **authentication** module handles user accounts, token issuance, OAuth provi }, { title: "Multi-tenant B2B", - outcome: "Teams with hierarchy, invites, and default team membership", + outcome: "Teams with hierarchy, invites, and ReBAC-backed membership", }, { title: "Secure web apps", outcome: "Server-side token vault — tokens never in browser storage", }, { - title: "Back-office access", - outcome: "Admin-team role gates for operator-only Admin API routes", + title: "Sensitive account changes", + outcome: "Sudo mode after fresh login gates password, email, 2FA, and team deletion", + }, + { + title: "CI and provisioning", + outcome: "Admin API `cdt_` tokens or MCP for operator workflows — not end-user tokens", }, ]} /> @@ -34,13 +44,17 @@ The **authentication** module handles user accounts, token issuance, OAuth provi @@ -49,10 +63,11 @@ The **authentication** module handles user accounts, token issuance, OAuth provi +Register returns `{ user }` only — the user must log in separately for tokens. + ## How it works -### Token lifecycle - -Access tokens (default 1h) authenticate Client API requests. When `refreshTokens.enabled` is true, `POST /authentication/renew` rotates the refresh token and issues a new pair. Blocked users (`active: false`) cannot renew or log in. - -### Application token storage - -**Web / Next.js:** Store `{ accessToken, refreshToken }` in a server-side vault (Redis). The browser holds only an opaque session cookie. On 401, renew once and retry. See the [Next.js guide](/docs/guides/first-app-nextjs). - -**Mobile:** Platform secure storage (Keychain, SecureStore) when no server vault exists. - -Never persist tokens in `localStorage` or `sessionStorage`. - -### Teams - -Teams register as a built-in ReBAC resource (`Team`) with relations `member`, `owner`, and team-scoped permissions. Team routes on the Client API require authentication; admin CRUD uses MCP tools like `get_authentication_teams` and `post_authentication_teams`. - -### OAuth - -When a provider is enabled in config, `GET /authentication/init/{provider}` redirects to the IdP. Callback hits `/authentication/hook/{provider}`; tokens return in JSON or httpOnly cookies depending on `accessTokens.setCookie` / `refreshTokens.setCookie`. + ## Configure @@ -100,14 +99,25 @@ Patch module config via MCP `patch_config_authentication`: | Key | Default | Meaning | |-----|---------|---------| | `accessTokens.expiryPeriod` | 3600000 (1h) | Access JWT lifetime (ms) | +| `accessTokens.setCookie` | false | Return tokens as httpOnly cookies | | `refreshTokens.enabled` | true | Enable `/authentication/renew` | | `refreshTokens.expiryPeriod` | 604800000 (7d) | Refresh token lifetime | -| `local.verification.required` | false | Require email verification before login | -| `teams.enabled` | false | Enable team routes and team-scoped ReBAC | - -OAuth providers need `clientId` + `clientSecret` per provider under `config.{provider}`. - -Local and OAuth routes require **router client id/secret** in request context (Conduit router client credentials). +| `local.enabled` | true | Email/password strategy | +| `local.verification.required` | false | Block login until email verified | +| `local.verification.method` | `link` | `link` or `code` verification | +| `teams.enabled` | false | Team routes and team-scoped ReBAC | +| `teams.invites.enabled` | false | Email team invitations | +| `teams.enableDefaultTeam` | false | Auto-create personal team on register | +| `twoFa.enabled` | false | Global 2FA toggle | +| `twoFa.methods.sms` | false | SMS 2FA (requires communications module) | +| `twoFa.methods.authenticator` | true | TOTP authenticator 2FA | +| `twoFa.backUpCodes.enabled` | true | Expose backup-code generate/recover routes | +| `magic_link.enabled` | false | Passwordless email login | +| `anonymousUsers.enabled` | false | Anonymous user registration | + +OAuth providers need `clientId` + `clientSecret` per provider under `config.{provider}.enabled`. + +Local and OAuth routes require **router client id/secret** in request context (Conduit router client credentials). Email verification, magic links, and invite emails require the **communications** module. ## Client API @@ -118,9 +128,16 @@ Local and OAuth routes require **router client id/secret** in request context (C | Renew | `POST /authentication/renew` | | Logout | `POST /authentication/logout` | | Current user | `GET /authentication/user` | -| Teams | `GET /authentication/teams`, `POST /authentication/teams`, … | - -Register returns `{ user }` only — the user must log in separately for tokens. +| Update user | `PATCH /authentication/user` | +| Delete user | `DELETE /authentication/user` (sudo) | +| Change password | `POST /authentication/local/change-password` (sudo) | +| Forgot / reset password | `POST /authentication/forgot-password`, `POST /authentication/reset-password` | +| Magic link | `POST /authentication/magic-link` | +| OAuth init | `GET /authentication/init/{provider}` | +| 2FA verify | `POST /authentication/twoFa/verify` | +| 2FA enable / disable | `PUT /authentication/twoFa/enable`, `PUT /authentication/twoFa/disable` (sudo) | +| 2FA backup codes | `GET /authentication/twoFa/generate`, `POST /authentication/twoFa/recover` (when `twoFa.backUpCodes.enabled`) | +| Teams | `GET /authentication/teams`, `POST /authentication/teams`, `GET /authentication/teams/:teamId`, … | ## MCP @@ -132,11 +149,14 @@ Enable with `?modules=authentication` in your MCP server URL. | `post_authentication_users` | Create user (admin) | | `get_authentication_teams` | List teams | | `post_authentication_teams` | Create team | -| `patch_config_authentication` | Token, OAuth, team settings | +| `patch_config_authentication` | Token, OAuth, team, and 2FA settings | + +For operator automation, prefer `cdt_` API tokens over legacy service-account config. See [MCP setup](/docs/getting-started/mcp-setup). diff --git a/apps/web/src/mdx/deep-dives/authentication-deep-dive.mdx b/apps/web/src/mdx/deep-dives/authentication-deep-dive.mdx new file mode 100644 index 00000000..e51f357e --- /dev/null +++ b/apps/web/src/mdx/deep-dives/authentication-deep-dive.mdx @@ -0,0 +1,114 @@ +### Token lifecycle + +Access tokens are JWTs (default **1h** via `accessTokens.expiryPeriod`). The payload includes: + +| Claim | Meaning | +|-------|---------| +| `id` | User ID | +| `authorized` | `true` when login is complete (including 2FA if enabled) | +| `sudo` | `true` on tokens issued from a **fresh** login — required for sensitive routes | + +When `refreshTokens.enabled` is true, `POST /authentication/renew` rotates the refresh token and issues a new pair. Renewed tokens set `sudo: false` — the user must re-authenticate for destructive operations. Blocked users (`active: false`) cannot renew or log in. + +`clients.multipleUserSessions` and `clients.multipleClientLogins` control whether new logins invalidate existing tokens per client or globally. + +### Sudo mode + +Several routes require `jwtPayload.sudo === true` on the access token. Sudo is granted only when: + +1. The user completed login (and 2FA, if enabled), and +2. Tokens were issued from a **login** flow — not from `/authentication/renew` + +If sudo is missing, the API returns `PERMISSION_DENIED` with *"Re-login required to enter sudo mode"*. + +| Route | Action | +|-------|--------| +| `DELETE /authentication/user` | Delete own account | +| `POST /authentication/local/change-password` | Change password | +| `POST /authentication/local/change-email` | Change email | +| `PUT /authentication/twoFa/enable` | Enable 2FA | +| `PUT /authentication/twoFa/disable` | Disable 2FA | +| `GET /authentication/twoFa/generate` | Generate backup codes | +| `DELETE /authentication/teams/:teamId` | Delete team | + +**Pattern:** prompt the user to log in again before sensitive settings changes; use the new access token for the sudo-gated call. + +### Application token storage + +**Web / Next.js:** Store `{ accessToken, refreshToken }` in a server-side vault (Redis). The browser holds only an opaque session cookie. On 401, renew once and retry. See the [Next.js guide](/docs/guides/first-app-nextjs). + +**Mobile:** Platform secure storage (Keychain, SecureStore) when no server vault exists. + +Never persist tokens in `localStorage` or `sessionStorage`. + +### Two-factor authentication (2FA) + +Enable globally with `twoFa.enabled`. Per-method toggles: + +| Config key | Method value | Notes | +|------------|--------------|-------| +| `twoFa.methods.authenticator` | `"authenticator"` | TOTP via authenticator app | +| `twoFa.methods.sms` | `"sms"` | SMS via the **communications** module — use method name **`sms`**, not `phone` | + +**Login with 2FA:** `POST /authentication/local` returns a challenge (SMS sent or OTP required) instead of full tokens. Complete with `POST /authentication/twoFa/verify` and the code. + +**Enable flow:** `PUT /authentication/twoFa/enable` with `{ "method": "sms", "phoneNumber": "+1..." }` or `{ "method": "authenticator" }` (requires sudo). Confirm via `POST /authentication/twoFa/enable/verify`. + +**Backup codes:** When `twoFa.backUpCodes.enabled` is `true`, these routes are registered: + +| Route | Purpose | +|-------|---------| +| `GET /authentication/twoFa/generate` | Generate 10 one-time codes (requires sudo) | +| `POST /authentication/twoFa/recover` | Recover access with an 8-digit backup code during login | + +When `twoFa.backUpCodes.enabled` is `false`, backup-code routes are **not** exposed. + +### Teams + +Enable with `teams.enabled`. Teams register as a built-in ReBAC resource (`Team`) with relations `member`, `owner`, and team-scoped permissions. See [Authorization](/docs/modules/authorization) and [ReBAC team scoping](/docs/guides/rebac-team-scoping). + +| Concept | Behavior | +|---------|----------| +| **Flat B2B** | Each root `Team` (`parentTeam: null`) is a tenant boundary | +| **Sub-teams** | `parentTeam` on create sets `Team:parent#owner@Team:child` for permission inheritance | +| **Invites** | `teams.invites.enabled` — email invites with `invitationToken` on register/OAuth | +| **Default team** | `teams.enableDefaultTeam` auto-creates a personal team per user | + +Key Client API routes (all require authentication): + +| Operation | Path | +|-----------|------| +| List my teams | `GET /authentication/teams` | +| Create team | `POST /authentication/teams` | +| Get / update / delete team | `GET\|PATCH\|DELETE /authentication/teams/:teamId` | +| Members | `GET\|PATCH\|DELETE /authentication/teams/:teamId/members` | +| Sub-teams | `GET /authentication/teams/:teamId/teams` | +| Invite user | `POST /authentication/teams/:teamId/invite` | +| Accept invite | `GET /authentication/teams/invite/accept?invitationToken=…` | +| Pending invites | `GET /authentication/teams/invites` | + +Team **delete** requires sudo. Admin CRUD uses MCP tools like `get_authentication_teams` and `post_authentication_teams`. + +### OAuth + +When a provider is enabled with `clientId` + `clientSecret`, routes are registered per provider: + +| Step | Path | +|------|------| +| Start redirect flow | `GET /authentication/init/{provider}` | +| Native/mobile init | `GET /authentication/initNative/{provider}` | +| IdP callback | `GET /authentication/hook/{provider}` | + +Supported providers include **Google**, **GitHub**, **Apple**, **Microsoft**, **Facebook**, **Slack**, **GitLab**, **Twitter**, **Twitch**, **Figma**, **Reddit**, **Bitbucket**, **LinkedIn**, and **Metamask** (when configured). + +Pass `invitationToken` and `redirectUri` on init to join a team during OAuth sign-up. Tokens return in JSON or httpOnly cookies depending on `accessTokens.setCookie` / `refreshTokens.setCookie`. `redirectUris.whitelistedUris` restricts post-auth redirects. + +### Machine auth (service accounts removed in v0.17) + +v0.17 **removes** Client API service accounts (`POST /authentication/service`) as a **breaking change**. The `Service` collection is dropped on authentication module startup migration. For automation, CI, and MCP clients, use **[Admin API tokens](/docs/reference/admin-api)** instead: + +- Create `cdt_` API tokens via Admin API (`POST /api-tokens`) or MCP `post_apitokens` +- Authenticate MCP with `Authorization: Bearer ` +- Never embed admin tokens in application runtime code or browser config + +Until you upgrade from v0.16, legacy deployments may still expose service-account login behind `service.enabled` — plan migration before promoting v0.17. See [Migration v0.16 → v0.17](/docs/resources/migration-v0.16-to-v0.17).