Skip to content

Commit f665d6a

Browse files
committed
docs(auth): document phone sign-in, admin user management, and bulk user import
Catches content/docs/permissions/authentication.mdx up with the shipped auth surface (#2766 / PR #2771, SMS follow-up #2780 / PR #2797): - Phone-Number Sign-In section: plugins.phoneNumber flag (and the CLI OS_AUTH_PHONE_NUMBER_ENABLED override), /sign-in/phone-number, the OTP flows' dependency on a wired SMS service (rate-limited), and the placeholder-email semantics for phone-only accounts. - Admin User Management section: /admin/create-user (one-time temporary password + mustChangePassword → 403 PASSWORD_EXPIRED gate), /admin/set-user-password, /admin/import-users (invite/temporary policies incl. the SMS-invite path, upsert profile-field-only rules, 500-row sync cap, no undo). - API endpoint reference: new Phone Number and Admin User Management subsections. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016r6eiJzivw1CkwTDSGho1o
1 parent 1c19139 commit f665d6a

1 file changed

Lines changed: 126 additions & 0 deletions

File tree

‎content/docs/permissions/authentication.mdx‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,59 @@ const response = await fetch('http://localhost:3000/api/v1/auth/magic-link/send'
488488
const response = await fetch(`http://localhost:3000/api/v1/auth/magic-link/verify?token=${token}`);
489489
```
490490

491+
### Phone-Number Sign-In
492+
493+
Enable phone numbers as a first-class sign-in identifier (better-auth
494+
`phone-number` plugin). Opt-in — off by default:
495+
496+
```typescript
497+
new AuthPlugin({
498+
secret: process.env.OS_AUTH_SECRET,
499+
baseUrl: 'http://localhost:3000',
500+
plugins: {
501+
phoneNumber: true, // Enable phone sign-in
502+
}
503+
})
504+
```
505+
506+
With the CLI `serve` command, `OS_AUTH_PHONE_NUMBER_ENABLED=true` flips the
507+
same flag. The plugin adds `sys_user.phone_number` (unique) and
508+
`sys_user.phone_number_verified`; the login UI discovers the capability via
509+
`features.phoneNumber` on `GET /api/v1/auth/config`.
510+
511+
#### Sign In with Phone + Password
512+
513+
```typescript
514+
const response = await fetch('http://localhost:3000/api/v1/auth/sign-in/phone-number', {
515+
method: 'POST',
516+
headers: { 'Content-Type': 'application/json' },
517+
body: JSON.stringify({
518+
phoneNumber: '+8613800000000',
519+
password: 'password123'
520+
})
521+
});
522+
```
523+
524+
#### OTP Flows (Require an SMS Service)
525+
526+
The OTP surfaces — `POST /phone-number/send-otp` + `POST /phone-number/verify`
527+
(sign-in / verification) and `POST /phone-number/request-password-reset` +
528+
`POST /phone-number/reset-password` (self-service reset) — only work when an
529+
SMS service is wired (`@objectstack/service-sms` registers the `sms` kernel
530+
service). Without one, `send-otp` fails loudly instead of silently dropping
531+
the code; phone sign-in is then password-based only (an admin sets the
532+
password — see Admin User Management below). OTP requests are rate-limited
533+
per phone number to prevent SMS-pumping abuse.
534+
535+
#### Placeholder Emails for Phone-Only Accounts
536+
537+
better-auth requires a unique email per user, so accounts created with only a
538+
phone number get a generated placeholder address of the form
539+
`u-<random>@placeholder.invalid`. These are never deliverable (`.invalid` is
540+
an RFC 2606 reserved TLD), never derived from the phone number (so exports
541+
and logs can't leak it), and every auth mail callback (password reset,
542+
invitation, magic link) refuses them with a `PLACEHOLDER_EMAIL` error.
543+
491544
### Organizations (Multi-Tenant)
492545

493546
Enable organization/team support:
@@ -502,6 +555,64 @@ new AuthPlugin({
502555
})
503556
```
504557

558+
### Admin User Management
559+
560+
With `plugins: { admin: true }` (forced on when SCIM is enabled), platform
561+
admins get email-independent account management on the Users list — the
562+
`create_user` / `set_user_password` actions there drive these endpoints. All
563+
three routes are gated server-side on the platform-admin signals
564+
(`isPlatformAdmin`, the `platform_admin` position, or the legacy `role`
565+
scalar) and drive the better-auth pipeline, so created accounts get a real
566+
scrypt-hashed credential and can sign in immediately.
567+
568+
#### Create a User Directly
569+
570+
```typescript
571+
const response = await fetch('http://localhost:3000/api/v1/auth/admin/create-user', {
572+
method: 'POST',
573+
headers: { 'Content-Type': 'application/json' },
574+
body: JSON.stringify({
575+
email: 'new.user@example.com', // and/or phoneNumber (requires plugins.phoneNumber)
576+
name: 'New User',
577+
generatePassword: true, // or pass an explicit `password`
578+
mustChangePassword: true // default true
579+
})
580+
});
581+
// → { success, data: { user, temporaryPassword? } }
582+
```
583+
584+
A generated temporary password is returned **once** in the response (the
585+
Users-list action reveals it in a one-shot dialog) and is never persisted or
586+
logged. While `mustChangePassword` is set, the user is blocked with
587+
`403 PASSWORD_EXPIRED` on every protected route until they complete a
588+
password change — the same gate and Console redirect as password expiry.
589+
590+
#### Set / Reset a Password
591+
592+
`POST /api/v1/auth/admin/set-user-password` accepts
593+
`{ userId, newPassword }` or `{ userId, generatePassword: true }` plus the
594+
same `mustChangePassword` flag. It also provisions a credential account for
595+
SSO-onboarded users who never had a local password.
596+
597+
#### Bulk Import Users
598+
599+
`POST /api/v1/auth/admin/import-users` accepts the same payload shapes as the
600+
generic data import (`rows[]` / `csv` / `xlsxBase64`, `dryRun`), but writes
601+
every row through better-auth so the accounts are login-capable:
602+
603+
- `passwordPolicy: 'invite'` — rows need a reachable identity: a real email
604+
(invitation goes out as a set-your-password email; requires an email
605+
service) or, for phone-only rows, a wired SMS service (invitation SMS +
606+
phone-OTP first sign-in).
607+
- `passwordPolicy: 'temporary'` — per-row generated temporary passwords,
608+
returned once in the response, with `mustChangePassword` enforced.
609+
- `mode: 'insert' | 'upsert'` with `matchBy: 'email' | 'phone'`. Upsert
610+
updates only touch profile fields (`name`, `phone_number`, `role`) — a
611+
re-imported file can never modify an existing user's email or reset their
612+
password.
613+
- Synchronous only, at most 500 rows per request (password hashing is
614+
CPU-bound); split larger files into batches. Imports are not undoable.
615+
505616
---
506617

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

727+
#### Phone Number (requires `plugins.phoneNumber`)
728+
729+
- `POST /api/v1/auth/sign-in/phone-number` - Sign in with phone + password
730+
- `POST /api/v1/auth/phone-number/send-otp` - Send a sign-in/verification OTP (requires an SMS service; rate-limited)
731+
- `POST /api/v1/auth/phone-number/verify` - Verify an OTP
732+
- `POST /api/v1/auth/phone-number/request-password-reset` - Request an OTP-based password reset (requires an SMS service)
733+
- `POST /api/v1/auth/phone-number/reset-password` - Complete an OTP-based password reset
734+
735+
#### Admin User Management (requires `plugins.admin`; platform-admin gated)
736+
737+
- `POST /api/v1/auth/admin/create-user` - Create a login-capable account (optional one-time temporary password + forced change)
738+
- `POST /api/v1/auth/admin/set-user-password` - Set/reset a user's password (also provisions a credential for SSO-onboarded users)
739+
- `POST /api/v1/auth/admin/import-users` - Bulk import users (CSV/JSON/XLSX; invite or temporary password policy; ≤500 rows, dry-run supported)
740+
- `POST /api/v1/auth/admin/unlock-user` - Clear a brute-force lockout early
741+
616742
For complete API documentation, see the [Better-Auth API Reference](https://www.better-auth.com/docs).
617743

618744
---

0 commit comments

Comments
 (0)