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
126 changes: 126 additions & 0 deletions content/docs/permissions/authentication.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,59 @@ const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send'
const response = await fetch(`http://localhost:3000/api/v1/auth/magic-link/verify?token=${token}`);
```

### Phone-Number Sign-In

Enable phone numbers as a first-class sign-in identifier (better-auth
`phone-number` plugin). Opt-in — off by default:

```typescript
new AuthPlugin({
secret: process.env.OS_AUTH_SECRET,
baseUrl: 'http://localhost:3000',
plugins: {
phoneNumber: true, // Enable phone sign-in
}
})
```

With the CLI `serve` command, `OS_AUTH_PHONE_NUMBER_ENABLED=true` flips the
same flag. The plugin adds `sys_user.phone_number` (unique) and
`sys_user.phone_number_verified`; the login UI discovers the capability via
`features.phoneNumber` on `GET /api/v1/auth/config`.

#### Sign In with Phone + Password

```typescript
const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
phoneNumber: '+8613800000000',
password: 'password123'
})
});
```

#### OTP Flows (Require an SMS Service)

The OTP surfaces — `POST /phone-number/send-otp` + `POST /phone-number/verify`
(sign-in / verification) and `POST /phone-number/request-password-reset` +
`POST /phone-number/reset-password` (self-service reset) — only work when an
SMS service is wired (`@objectstack/service-sms` registers the `sms` kernel
service). Without one, `send-otp` fails loudly instead of silently dropping
the code; phone sign-in is then password-based only (an admin sets the
password — see Admin User Management below). OTP requests are rate-limited
per phone number to prevent SMS-pumping abuse.

#### Placeholder Emails for Phone-Only Accounts

better-auth requires a unique email per user, so accounts created with only a
phone number get a generated placeholder address of the form
`u-<random>@placeholder.invalid`. These are never deliverable (`.invalid` is
an RFC 2606 reserved TLD), never derived from the phone number (so exports
and logs can't leak it), and every auth mail callback (password reset,
invitation, magic link) refuses them with a `PLACEHOLDER_EMAIL` error.

### Organizations (Multi-Tenant)

Enable organization/team support:
Expand All @@ -502,6 +555,64 @@ new AuthPlugin({
})
```

### Admin User Management

With `plugins: { admin: true }` (forced on when SCIM is enabled), platform
admins get email-independent account management on the Users list — the
`create_user` / `set_user_password` actions there drive these endpoints. All
three routes are gated server-side on the platform-admin signals
(`isPlatformAdmin`, the `platform_admin` position, or the legacy `role`
scalar) and drive the better-auth pipeline, so created accounts get a real
scrypt-hashed credential and can sign in immediately.

#### Create a User Directly

```typescript
const response = await fetch('http://localhost:3000/api/v1/auth/admin/create-user', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: 'new.user@example.com', // and/or phoneNumber (requires plugins.phoneNumber)
name: 'New User',
generatePassword: true, // or pass an explicit `password`
mustChangePassword: true // default true
})
});
// → { success, data: { user, temporaryPassword? } }
```

A generated temporary password is returned **once** in the response (the
Users-list action reveals it in a one-shot dialog) and is never persisted or
logged. While `mustChangePassword` is set, the user is blocked with
`403 PASSWORD_EXPIRED` on every protected route until they complete a
password change — the same gate and Console redirect as password expiry.

#### Set / Reset a Password

`POST /api/v1/auth/admin/set-user-password` accepts
`{ userId, newPassword }` or `{ userId, generatePassword: true }` plus the
same `mustChangePassword` flag. It also provisions a credential account for
SSO-onboarded users who never had a local password.

#### Bulk Import Users

`POST /api/v1/auth/admin/import-users` accepts the same payload shapes as the
generic data import (`rows[]` / `csv` / `xlsxBase64`, `dryRun`), but writes
every row through better-auth so the accounts are login-capable:

- `passwordPolicy: 'invite'` — rows need a reachable identity: a real email
(invitation goes out as a set-your-password email; requires an email
service) or, for phone-only rows, a wired SMS service (invitation SMS +
phone-OTP first sign-in).
- `passwordPolicy: 'temporary'` — per-row generated temporary passwords,
returned once in the response, with `mustChangePassword` enforced.
- `mode: 'insert' | 'upsert'` with `matchBy: 'email' | 'phone'`. Upsert
updates only touch profile fields (`name`, `phone_number`, `role`) — a
re-imported file can never modify an existing user's email or reset their
password.
- Synchronous only, at most 500 rows per request (password hashing is
CPU-bound); split larger files into batches. Imports are not undoable.

---

## Client Integration
Expand Down Expand Up @@ -613,6 +724,21 @@ All endpoints are available under `/api/v1/auth/*`:
- `POST /api/v1/auth/magic-link/send` - Send magic link email
- `GET /api/v1/auth/magic-link/verify` - Verify magic link

#### Phone Number (requires `plugins.phoneNumber`)

- `POST /api/v1/auth/sign-in/phone-number` - Sign in with phone + password
- `POST /api/v1/auth/phone-number/send-otp` - Send a sign-in/verification OTP (requires an SMS service; rate-limited)
- `POST /api/v1/auth/phone-number/verify` - Verify an OTP
- `POST /api/v1/auth/phone-number/request-password-reset` - Request an OTP-based password reset (requires an SMS service)
- `POST /api/v1/auth/phone-number/reset-password` - Complete an OTP-based password reset

#### Admin User Management (requires `plugins.admin`; platform-admin gated)

- `POST /api/v1/auth/admin/create-user` - Create a login-capable account (optional one-time temporary password + forced change)
- `POST /api/v1/auth/admin/set-user-password` - Set/reset a user's password (also provisions a credential for SSO-onboarded users)
- `POST /api/v1/auth/admin/import-users` - Bulk import users (CSV/JSON/XLSX; invite or temporary password policy; ≤500 rows, dry-run supported)
- `POST /api/v1/auth/admin/unlock-user` - Clear a brute-force lockout early

For complete API documentation, see the [Better-Auth API Reference](https://www.better-auth.com/docs).

---
Expand Down
2 changes: 2 additions & 0 deletions content/docs/references/api/auth-endpoints.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ const result = AuthEndpoint.parse(data);
| **magicLink** | `boolean` | ✅ | Magic link login enabled |
| **organization** | `boolean` | ✅ | Multi-tenant organization support enabled |
| **ssoEnforced** | `boolean` | optional | SSO-only login enforced: the UI hides the local password form + self-registration (a break-glass "use a password" link remains) |
| **phoneNumber** | `boolean` | optional | Phone-number sign-in enabled (phone + password, #2766 V1.5) |
| **phoneNumberOtp** | `boolean` | optional | Phone-number OTP sign-in and self-service password reset available — requires the phoneNumber plugin plus a deliverable SMS service (#2780) |


---
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/system/auth-config.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Advanced / low-level Better-Auth options
| **dynamicClientRegistration** | `boolean` | optional | Allow unauthenticated RFC 7591 Dynamic Client Registration (default: follows OS_MCP_SERVER_ENABLED) |
| **deviceAuthorization** | `boolean` | ✅ | Enable RFC 8628 Device Authorization Grant (CLI / TV-style login) |
| **admin** | `boolean` | ✅ | Enable platform admin operations (ban/unban, set-password, impersonate, set-role) |
| **phoneNumber** | `boolean` | ✅ | Enable phone-number sign-in (phone + password; OTP sign-in/reset when an SMS service is configured) |


---
Expand Down
1 change: 1 addition & 0 deletions content/docs/references/ui/component.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ Type: `string`
| :--- | :--- | :--- | :--- |
| **objectName** | `string` | ✅ | Related object name (e.g., "task", "opportunity") |
| **relationshipField** | `string` | ✅ | Field on related object that points to this record (e.g., "account_id") |
| **relationshipValueField** | `string` | ✅ | Parent-record field whose value relationshipField stores (default 'id'; e.g. 'name' for name-keyed junctions). |
| **columns** | `string[]` | optional | Fields to display in the related list. Optional: when omitted, columns derive from the related object's highlightFields / default list columns (a related list is just another surface that lists that object). Override chain: child highlightFields → field-level relatedListColumns → this inline list. |
| **sort** | `string \| Object[]` | optional | Sort order for related records |
| **limit** | `integer` | ✅ | Number of records to display initially |
Expand Down
1 change: 1 addition & 0 deletions scripts/role-word-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"content/docs/kernel/runtime-services/examples.mdx": 2,
"content/docs/kernel/runtime-services/sharing-service.mdx": 2,
"content/docs/kernel/services-checklist.mdx": 5,
"content/docs/permissions/authentication.mdx": 2,
"content/docs/permissions/authorization.mdx": 3,
"content/docs/permissions/index.mdx": 1,
"content/docs/permissions/permission-metadata.mdx": 2,
Expand Down