Authentication
Secure your SonicJS application with JWT authentication and role-based access control.
Overview
SonicJS uses JWT (JSON Web Tokens) for authentication with:
- 24-hour token expiration
- HTTP-only cookie storage for web clients
- Bearer token support for API clients
- KV-based token caching (5-minute TTL)
- Role-based access control (RBAC)
- Permission system for fine-grained access
JWT Tokens
Secure, stateless authentication with automatic expiration
User Roles
Admin, Editor, Author, and Viewer roles with different permissions
Fast Verification
KV cache for sub-millisecond token verification
Secure by Default
HTTP-only cookies, CSRF protection, and rate limiting
JWT Authentication
Login
/auth/loginLogin Request
curl -X POST http://localhost:8787/auth/login \
-H "Content-Type: application/json" \
-d '{
"email": "admin@sonicjs.com",
"password": "sonicjs!"
}'
Response (200 OK):
{
"user": {
"id": "admin-user-id",
"email": "admin@sonicjs.com",
"username": "admin",
"firstName": "Admin",
"lastName": "User",
"role": "admin"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Using the Token
Include the token in the Authorization header for authenticated requests:
Authenticated Request
curl http://localhost:8787/admin/content \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
For browser-based applications, the token is automatically stored as an HTTP-only cookie named auth_token.
Passwordless Authentication
SonicJS offers multiple passwordless authentication methods through plugins. These plugins are inactive by default and need to be enabled in the admin panel.
OTP Login Plugin
The OTP (One-Time Password) Login Plugin provides email-based authentication with temporary codes.
Features:
- Configurable OTP code length (4-8 digits)
- Code expiration (5-60 minutes, default: 10 minutes)
- Rate limiting (default: 5 codes per hour)
- Max verification attempts (default: 3)
- Optional new user registration support
Installation:
- Navigate to Admin → Plugins
- Find OTP Login plugin
- Click Activate
- Configure settings:
- Code Length: 4-8 digits (default: 6)
- Code Expiration: 5-60 minutes (default: 10)
- Max Attempts: 3-10 (default: 3)
- Rate Limit: Codes per hour (default: 5)
- Allow Registration: Enable if you want new users to register via OTP
API Usage:
OTP Login Flow
# Step 1: Request OTP code
curl -X POST http://localhost:8787/auth/otp/request \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
# Response: { "message": "OTP code sent to email" }
Database Schema:
The OTP plugin creates a new table otp_codes:
CREATE TABLE otp_codes (
id TEXT PRIMARY KEY,
user_email TEXT NOT NULL,
code TEXT NOT NULL,
expires_at INTEGER NOT NULL,
attempts INTEGER DEFAULT 0,
created_at INTEGER DEFAULT (unixepoch())
);
Magic Link Authentication Plugin
The Magic Link Plugin enables passwordless login via email links.
Features:
- One-click email authentication
- No password required
- Secure token-based links
- Optional user registration
Installation:
- Navigate to Admin → Plugins
- Find Magic Link Auth plugin
- Click Activate
- Configure email settings (requires Email Plugin)
API Usage:
Magic Link Flow
# Step 1: Request magic link
curl -X POST http://localhost:8787/auth/magic-link/request \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com"
}'
# Response: { "message": "Magic link sent to email" }
Choosing an Authentication Method
| Method | Best For | Security | User Experience |
|---|---|---|---|
| Email + Password | Traditional apps, maximum control | ⭐⭐⭐⭐ | Familiar, requires password management |
| OTP Login | Mobile apps, high-security environments | ⭐⭐⭐⭐⭐ | Easy, no password needed, limited time |
| Magic Link | Simplified onboarding, newsletters | ⭐⭐⭐⭐ | Seamless, requires email access |
Email Plugin Required: Both OTP and Magic Link authentication require the Email Plugin to be configured with a valid email service (e.g., Resend, SendGrid).
OAuth / Social Login
SonicJS supports OAuth2 social login through the OAuth Providers plugin, allowing users to sign in with GitHub, Google, and other providers using the standard authorization code flow.
Supported Providers
| Provider | Scopes | Profile Data |
|---|---|---|
| GitHub | read:user, user:email | ID, email, name, avatar |
openid, email, profile | ID, email, name, avatar |
Quick Setup
- Create an OAuth app on the provider (GitHub or Google)
- Navigate to Admin > Plugins > OAuth Providers
- Enter your Client ID and Client Secret for each provider
- Toggle Enable and save
OAuth Endpoints
/auth/oauth/:provider/auth/oauth/:provider/callback/auth/oauth/accountsAuth required/auth/oauth/linkAuth required/auth/oauth/unlinkAuth requiredUsage
Add social login buttons that point to the OAuth authorization endpoints:
Social Login
<a href="/auth/oauth/github">Sign in with GitHub</a>
<a href="/auth/oauth/google">Sign in with Google</a>
When a user authenticates via OAuth:
- Existing OAuth link — the user is logged in directly
- Email matches an existing account — the OAuth provider is automatically linked to the existing account
- New user — an account is created from the OAuth profile with the
viewerrole
Account Linking and Unlinking
Authenticated users can link additional OAuth providers or unlink existing ones:
Account Linking
curl -X POST http://localhost:8787/auth/oauth/link \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{ "provider": "google" }'
# Response: { "redirectUrl": "https://accounts.google.com/..." }
# Redirect the user to the returned URL to complete linking
A user cannot unlink their only authentication method. If they have no password and only one OAuth link, unlinking is blocked until they set a password or link another provider.
Choosing an Authentication Method (Updated)
| Method | Best For | Security | User Experience |
|---|---|---|---|
| Email + Password | Traditional apps, maximum control | High | Familiar, requires password management |
| OTP Login | Mobile apps, high-security environments | Very High | Easy, no password needed, limited time |
| Magic Link | Simplified onboarding, newsletters | High | Seamless, requires email access |
| OAuth / Social | Consumer apps, developer tools | High | One-click, no new credentials needed |
User Management
User Registration
/auth/registerRegister User
curl -X POST http://localhost:8787/auth/register \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword123",
"username": "newuser",
"firstName": "John",
"lastName": "Doe"
}'
User Profiles
SonicJS includes configurable user profiles that let you define custom fields for your users without modifying database migrations. Custom data is stored as JSON in the user_profiles.data column and rendered automatically in the admin UI.
Configuring Custom Profile Fields
Define custom fields at app boot using defineUserProfile():
Profile Configuration
import { SonicJS, defineUserProfile } from "@sonicjs-cms/core"
defineUserProfile({
fields: [
{
name: "plan",
label: "Subscription Plan",
type: "select",
options: ["free", "pro", "enterprise"],
default: "free"
},
{
name: "company_size",
label: "Company Size",
type: "number",
required: true,
validation: { min: 1, max: 10000 }
},
{
name: "industry",
label: "Industry",
type: "select",
options: ["tech", "healthcare", "finance", "education", "other"]
},
{
name: "onboarding_completed",
label: "Onboarding Completed",
type: "boolean",
default: false
}
],
// Optional: which custom fields appear on the registration form
registrationFields: ["plan", "company_size"],
})
const app = SonicJS({ /* ... */ })
Custom fields support the same field types as collections: string, number, boolean, select, textarea, json, and object (for nested fields).
Field Definition Properties
- Name
name- Type
- string
- Description
Machine name used as the JSON key when storing data.
- Name
label- Type
- string
- Description
Human-readable label displayed in the admin UI.
- Name
type- Type
- string
- Description
Field type — reuses the same types available in collections.
- Name
options- Type
- string[]
- Description
Available choices for
select,multiselect, orradiofield types.
- Name
default- Type
- any
- Description
Default value applied when creating new profiles.
- Name
required- Type
- boolean
- Description
Whether the field must have a value on save. Defaults to
false.
- Name
validation- Type
- object
- Description
Constraints:
min/maxfor numbers,pattern(regex) for strings.
- Name
hidden- Type
- boolean
- Description
When
true, the field is available via API but hidden from the admin UI.
Profile API Endpoints
/api/user-profiles/schemaReturns the configured field schema so frontend apps can render profile forms dynamically.
/api/user-profiles/:userIdAuth required{
"userId": "usr-xyz",
"customData": {
"plan": "pro",
"company_size": 50,
"industry": "tech"
}
}
/api/user-profiles/:userIdAuth requiredUpdate Profile
curl -X PUT http://localhost:8787/api/user-profiles/usr-xyz \
-H "Authorization: Bearer eyJhbGc..." \
-H "Content-Type: application/json" \
-d '{
"customData": {
"plan": "enterprise",
"company_size": 200
}
}'
Validation errors return a structured response:
{
"error": "Validation failed",
"errors": {
"plan": "Plan must be one of: free, pro, enterprise"
}
}
Admin UI Integration
Custom profile fields appear automatically in:
- User edit page (
/admin/users/:id/edit) — admins can edit any user's custom fields - Profile page (
/admin/profile) — users can edit their own custom fields
Fields are rendered below the standard profile fields (display name, bio, company, etc.) using the same dynamic field renderer as collections.
Registration Form Integration
When registrationFields is specified in the config, those custom fields are included in the registration form at /auth/register. Defaults from the field config are applied for any fields not included in registrationFields.
Built-in Profile Fields
Every user profile includes these standard fields out of the box:
| Field | Type | Description |
|---|---|---|
display_name | string | Public display name |
bio | string | Short biography |
company | string | Company or organization |
job_title | string | Job title or role |
website | string | Personal or company website |
location | string | Geographic location |
date_of_birth | integer | Date of birth (unix timestamp) |
Custom fields defined via defineUserProfile() extend these — they do not replace them.
RBAC
User Roles
SonicJS supports four built-in roles with specific enforcement across the system:
- Name
admin- Type
- string
- Description
Full system access. Can manage users, collections, content, settings, and plugins.
- Name
editor- Type
- string
- Description
Content management. Can create, edit, publish, and delete content. Can read collections but not create or modify them.
- Name
author- Type
- string
- Description
Own content only. Can create content and edit their own items. Cannot publish or delete. Limited to draft/review states.
- Name
viewer- Type
- string
- Description
Read-only access. Can view published content but cannot create, edit, or delete anything.
Permission Matrix
| Capability | Admin | Editor | Author | Viewer |
|---|---|---|---|---|
| Create/edit/delete users | Yes | No | No | No |
| Create/edit/delete collections | Yes | No | No | No |
| Create content | Yes | Yes | Yes | No |
| Edit any content | Yes | Yes | No | No |
| Edit own content | Yes | Yes | Yes | No |
| Publish content | Yes | Yes | No | No |
| Delete content | Yes | Yes (own) | No | No |
| View activity logs | Yes | No | No | No |
| Manage plugins | Yes | No | No | No |
| Access admin panel | Yes | Yes | Yes | Limited |
Default Roles
- The first registered user is automatically assigned the
adminrole - All subsequent self-registered users default to the
viewerrole - Admins can change user roles via the admin panel
Middleware
Protect routes with authentication and role-based middleware:
Middleware Protection
// Require authentication
app.use('/admin/*', requireAuth())
// Require specific role
app.use('/admin/*', requireRole(['admin', 'editor']))
// Require permission
app.use('/admin/settings/*', requirePermission('manage:settings'))
// Optional authentication
app.use('/api/*', optionalAuth())
The requireRole() middleware returns a 403 JSON response for API requests or redirects to /auth/login for browser requests. Role checking uses constant-time comparison to prevent timing attacks.
Security
Password Hashing
SonicJS uses PBKDF2-SHA256 with strong parameters for password storage:
- 100,000 iterations (NIST-recommended minimum)
- 16-byte random salt per password
- 256-bit derived key
- Format:
pbkdf2:<iterations>:<salt_hex>:<hash_hex>
Legacy SHA-256 hashes are automatically upgraded to PBKDF2 on next successful login.
Rate Limiting
Authentication endpoints are protected with KV-based rate limiting:
| Endpoint | Limit | Window |
|---|---|---|
POST /auth/login | 5 attempts | 60 seconds |
POST /auth/register | 3 attempts | 60 seconds |
POST /auth/seed-admin | 2 attempts | 60 seconds |
POST /auth/request-password-reset | 3 attempts | 15 minutes |
Rate limiting uses Cloudflare KV for distributed state and gracefully degrades if KV is unavailable. Responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers.
CSRF Protection
SonicJS implements a signed double-submit cookie pattern for CSRF protection:
- Tokens are HMAC-SHA256 signed using the
JWT_SECRET - Validation checks the
X-CSRF-Tokenheader against thecsrf_tokencookie - Falls back to reading
_csrffrom form body for traditional form submissions - Cookie settings:
sameSite: 'Strict',secure: truein production, 24-hour expiry
Exempt paths: Login, registration, public form submissions, and API-key-only requests are exempt from CSRF validation.
Security Headers
All responses include security headers:
| Header | Value |
|---|---|
X-Content-Type-Options | nosniff |
X-Frame-Options | SAMEORIGIN |
Referrer-Policy | strict-origin-when-cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() |
Strict-Transport-Security | max-age=31536000; includeSubDomains (production) |