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

POST/auth/login
Authenticate user and receive JWT token

Login 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:

  1. Navigate to AdminPlugins
  2. Find OTP Login plugin
  3. Click Activate
  4. 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:

  1. Navigate to AdminPlugins
  2. Find Magic Link Auth plugin
  3. Click Activate
  4. 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

MethodBest ForSecurityUser Experience
Email + PasswordTraditional apps, maximum control⭐⭐⭐⭐Familiar, requires password management
OTP LoginMobile apps, high-security environments⭐⭐⭐⭐⭐Easy, no password needed, limited time
Magic LinkSimplified 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

ProviderScopesProfile Data
GitHubread:user, user:emailID, email, name, avatar
Googleopenid, email, profileID, email, name, avatar

Quick Setup

  1. Create an OAuth app on the provider (GitHub or Google)
  2. Navigate to Admin > Plugins > OAuth Providers
  3. Enter your Client ID and Client Secret for each provider
  4. Toggle Enable and save

OAuth Endpoints

GET/auth/oauth/:provider
Redirect to provider authorization page
GET/auth/oauth/:provider/callback
Handle OAuth callback and authenticate user
GET/auth/oauth/accountsAuth required
List linked OAuth accounts for current user
POST/auth/oauth/linkAuth required
Link an OAuth provider to current account
POST/auth/oauth/unlinkAuth required
Unlink an OAuth provider from current account

Usage

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 viewer role

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)

MethodBest ForSecurityUser Experience
Email + PasswordTraditional apps, maximum controlHighFamiliar, requires password management
OTP LoginMobile apps, high-security environmentsVery HighEasy, no password needed, limited time
Magic LinkSimplified onboarding, newslettersHighSeamless, requires email access
OAuth / SocialConsumer apps, developer toolsHighOne-click, no new credentials needed

User Management

User Registration

POST/auth/register
Create new user account

Register 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, or radio field 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/max for 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

GET/api/user-profiles/schema
Get the profile field definitions (public)

Returns the configured field schema so frontend apps can render profile forms dynamically.

GET/api/user-profiles/:userIdAuth required
Get custom profile data for a user
{
  "userId": "usr-xyz",
  "customData": {
    "plan": "pro",
    "company_size": 50,
    "industry": "tech"
  }
}
PUT/api/user-profiles/:userIdAuth required
Update custom profile data

Update 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:

FieldTypeDescription
display_namestringPublic display name
biostringShort biography
companystringCompany or organization
job_titlestringJob title or role
websitestringPersonal or company website
locationstringGeographic location
date_of_birthintegerDate 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

CapabilityAdminEditorAuthorViewer
Create/edit/delete usersYesNoNoNo
Create/edit/delete collectionsYesNoNoNo
Create contentYesYesYesNo
Edit any contentYesYesNoNo
Edit own contentYesYesYesNo
Publish contentYesYesNoNo
Delete contentYesYes (own)NoNo
View activity logsYesNoNoNo
Manage pluginsYesNoNoNo
Access admin panelYesYesYesLimited

Default Roles

  • The first registered user is automatically assigned the admin role
  • All subsequent self-registered users default to the viewer role
  • 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:

EndpointLimitWindow
POST /auth/login5 attempts60 seconds
POST /auth/register3 attempts60 seconds
POST /auth/seed-admin2 attempts60 seconds
POST /auth/request-password-reset3 attempts15 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-Token header against the csrf_token cookie
  • Falls back to reading _csrf from form body for traditional form submissions
  • Cookie settings: sameSite: 'Strict', secure: true in 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:

HeaderValue
X-Content-Type-Optionsnosniff
X-Frame-OptionsSAMEORIGIN
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policycamera=(), microphone=(), geolocation=()
Strict-Transport-Securitymax-age=31536000; includeSubDomains (production)

Next Steps

Was this page helpful?