ObjectOS LogoObjectOS

Security Guide

Security is a fundamental concern in ObjectOS. This guide covers authentication, authorization, permissions, and security best practices for building secure applications.

Security Architecture

ObjectOS implements a multi-layered security model aligned with OWASP Top 10 recommendations:

  1. Authentication (@objectos/auth): Verify user identity via Better-Auth
  2. Authorization (@objectos/permissions): Object, field, and record-level access control
  3. Field-Level Security: Control access to specific fields via permission sets
  4. Record-Level Security: Filter records via SharingRuleEngine and RLSEvaluator
  5. Audit Logging (@objectos/audit): Track all data changes with 34+ event types
  6. Security Headers: OWASP-compliant HTTP headers via Hono secureHeaders middleware

Authentication

Using Better-Auth

ObjectOS uses Better-Auth via the @objectos/auth plugin (BetterAuthPlugin), supporting multiple strategies:

  • Local (email/password)
  • OAuth 2.0 (Google, GitHub, Microsoft)
  • SAML (Enterprise SSO)
  • LDAP
  • Magic Links

Setup Authentication

Authentication is configured as a plugin in objectstack.config.ts:

import { BetterAuthPlugin } from '@objectos/auth';

export default {
  plugins: [
    new BetterAuthPlugin({
      // Optional security policies
      securityPolicies: {
        password: { minLength: 12, maxLength: 128 },
        session: { expiresIn: '7d', updateAge: '1d' },
      },
    }),
    // ... other plugins
  ],
};

Login Endpoint

All auth routes are under /api/v1/auth/*:

POST /api/v1/auth/sign-up/email
Content-Type: application/json

{
  "name": "John Doe",
  "email": "user@example.com",
  "password": "securepassword"
}
POST /api/v1/auth/sign-in/email
Content-Type: application/json

{
  "email": "user@example.com",
  "password": "securepassword"
}

Session Management

Better-Auth manages sessions via HTTP-only cookies. The Admin Console (apps/web) uses better-auth/react for session handling:

import { createAuthClient } from 'better-auth/react';

export const authClient = createAuthClient({
  baseURL: '/api/v1/auth',
});

Authorization

Object-Level Permissions

Control CRUD access to entire objects using YAML permission sets loaded by @objectos/permissions:

# permissions/contacts.permission.yml
name: contact-permissions
objectName: contacts
profiles:
  admin:
    allowRead: true
    allowCreate: true
    allowEdit: true
    allowDelete: true
  sales:
    allowRead: true
    allowCreate: true
    allowEdit: true
    allowDelete: false
  guest:
    allowRead: true
    allowCreate: false
    allowEdit: false
    allowDelete: false

Permission Checking

ObjectOS automatically checks permissions on all data operations via kernel hooks (data.beforeCreate, data.beforeUpdate, data.beforeDelete, data.beforeFind):

// The PermissionsPlugin hooks into data.before* events automatically
// A user without the required profile will receive a 403 error:
// Error: Permission denied for delete on contacts (code: PERMISSION_DENIED)

Programmatic Permission Checks

Use the REST API to check permissions:

POST /api/v1/permissions/check
Content-Type: application/json

{
  "userId": "user_123",
  "profileName": "sales",
  "objectName": "contacts",
  "action": "delete"
}

Response:

{
  "success": true,
  "data": { "hasPermission": false }
}

Field-Level Security

Control access to specific fields:

fields:
  salary:
    type: currency
    label: Salary
    visible_to: ['hr', 'admin'] # Only HR and admin can see
    editable_by: ['hr'] # Only HR can edit

  social_security:
    type: text
    label: SSN
    visible_to: ['hr'] # Only HR can see
    editable_by: [] # No one can edit (read-only)

Behavior:

  • Users without visible_to roles cannot see the field in queries or forms
  • Users without editable_by roles cannot update the field
  • Attempts to read/write restricted fields return 403 Forbidden

Record-Level Security (RLS)

The @objectos/permissions plugin provides RLSEvaluator and SharingRuleEngine for record-level security:

Organization-Wide Defaults

Define default access levels per object:

const rlsConfig = {
  organizationWideDefaults: {
    accounts: 'private', // Only owner can see
    contacts: 'publicRead', // Everyone can read
    opportunities: 'private',
  },
};

Sharing Rules

Define sharing rules in YAML metadata:

# permissions/opportunities.permission.yml
sharingRules:
  - name: Team Access
    type: criteria
    criteria:
      team: $currentUser.teamId
    accessLevel: readWrite

  - name: Manager Access
    type: owner
    ownerField: reports_to
    accessLevel: readOnly

The SharingRuleEngine evaluates these rules during data.beforeFind to inject record-level filters automatically.

Roles and Profiles

Defining Roles

Roles are defined in the auth system:

# objects/_roles.object.yml
name: _roles
label: Role
system: true

records:
  - name: admin
    label: Administrator
    description: Full system access

  - name: sales
    label: Sales User
    description: Sales team member

  - name: support
    label: Support User
    description: Customer support

  - name: guest
    label: Guest
    description: Limited read-only access

Assigning Roles

// Assign role to user
await kernel.update('users', userId, {
  roles: ['sales', 'support'],
});

Role Hierarchy

Define role inheritance:

role_hierarchy:
  admin:
    inherits: [sales, support, guest]

  sales:
    inherits: [guest]

  support:
    inherits: [guest]

Input Validation & Sanitization

Automatic Validation

ObjectOS automatically validates:

  • Required fields
  • Data types
  • Unique constraints
  • Min/max values
  • Pattern matching

Custom Validation

Add custom validation in hooks:

kernel.on('beforeInsert:contacts', async (ctx) => {
  // Validate email domain
  if (ctx.data.email && !ctx.data.email.endsWith('@company.com')) {
    throw new Error('Only company email addresses allowed');
  }

  // Sanitize phone number
  if (ctx.data.phone) {
    ctx.data.phone = ctx.data.phone.replace(/[^0-9+]/g, '');
  }
});

SQL Injection Prevention

ObjectOS drivers automatically parameterize queries:

// Safe - parameters are escaped
await kernel.find('contacts', {
  filters: {
    email: userInput, // Automatically sanitized
  },
});

XSS Prevention

Sanitize HTML content:

import { sanitizeHtml } from '@objectos/security';

kernel.on('beforeInsert', async (ctx) => {
  if (ctx.data.description) {
    ctx.data.description = sanitizeHtml(ctx.data.description);
  }
});

Audit Logging

The @objectos/audit plugin (AuditLogPlugin) automatically records all data operations and 34+ event types:

Configuration

import { AuditLogPlugin } from '@objectos/audit';

new AuditLogPlugin({
  enabled: true,
  trackFieldChanges: true,
  excludedFields: ['password', 'token', 'secret'],
  retention: {
    enabled: true,
    defaultRetentionDays: 90,
  },
});

Querying Audit Events

GET /api/v1/audit/events?objectName=contacts&userId=user_123

Audit Trail for a Record

GET /api/v1/audit/trail/contacts/contact_123

Field History

GET /api/v1/audit/field-history/contacts/contact_123/status

Event Types Covered

  • Data events: data.create, data.update, data.delete, data.read
  • Auth events: auth.login, auth.logout, auth.login_failed, auth.session_created, auth.password_changed, etc.
  • Authorization events: authz.role_assigned, authz.permission_granted, authz.role_created, etc.
  • System events: system.config_changed, system.plugin_installed, etc.
  • Security events: security.access_denied, security.suspicious_activity, etc.
  • Job events: job.enqueued, job.completed, job.failed, etc.

Rate Limiting

Prevent abuse with rate limiting:

import { RateLimitPlugin } from '@objectos/plugin-rate-limit';

kernel.use(
  RateLimitPlugin({
    windowMs: 60 * 1000, // 1 minute
    maxRequests: 100, // 100 requests per window
    perUser: true, // Rate limit per user
  }),
);

CORS Configuration

CORS is configured in objectstack.config.ts:

export default {
  server: {
    cors: {
      origin: process.env.CORS_ORIGINS?.split(',') || [
        'http://localhost:5321',
        'http://localhost:5320',
      ],
      credentials: true,
    },
  },
};

For the Vercel serverless deployment, CORS is applied via Hono middleware in api/index.ts.

API Key Authentication

For server-to-server communication:

kernel.use(
  ApiKeyPlugin({
    keys: [
      {
        key: process.env.API_KEY_1,
        name: 'Integration Server',
        scopes: ['read:contacts', 'write:contacts'],
      },
    ],
  }),
);

Usage:

GET /api/data/contacts
X-API-Key: your-api-key-here

Security Headers (OWASP Compliance)

ObjectOS uses Hono's built-in secureHeaders middleware for OWASP-compliant security headers:

import { secureHeaders } from 'hono/secure-headers';

honoApp.use(
  '/api/v1/*',
  secureHeaders({
    contentSecurityPolicy: {
      defaultSrc: ["'self'"],
      scriptSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      imgSrc: ["'self'", 'data:', 'https:'],
      connectSrc: ["'self'"],
      fontSrc: ["'self'"],
      objectSrc: ["'none'"],
      frameAncestors: ["'none'"],
    },
    crossOriginResourcePolicy: 'same-origin',
    referrerPolicy: 'strict-origin-when-cross-origin',
    xContentTypeOptions: 'nosniff',
    xFrameOptions: 'DENY',
  }),
);

Response Headers Applied

HeaderValueOWASP Reference
Content-Security-Policydefault-src 'self'; ...A05:2021 – Security Misconfiguration
X-Frame-OptionsDENYClickjacking protection
X-Content-Type-OptionsnosniffMIME-type sniffing prevention
Referrer-Policystrict-origin-when-cross-originInformation leakage prevention
Cross-Origin-Resource-Policysame-originCross-origin isolation

Encryption

Encrypting Sensitive Fields

import { encrypt, decrypt } from '@objectos/security';

kernel.on('beforeInsert', async (ctx) => {
  // Encrypt SSN before storing
  if (ctx.data.social_security) {
    ctx.data.social_security = encrypt(ctx.data.social_security);
  }
});

kernel.on('afterFind', async (ctx) => {
  // Decrypt SSN when retrieving
  ctx.result.forEach((record) => {
    if (record.social_security) {
      record.social_security = decrypt(record.social_security);
    }
  });
});

Environment Variables

Never hard-code secrets:

# .env
DATABASE_URL=postgresql://...
JWT_SECRET=your-secret-key-here
ENCRYPTION_KEY=your-encryption-key-here
API_KEY=your-api-key-here
// ❌ BAD
const secret = 'my-secret-key';

// ✅ GOOD
const secret = process.env.JWT_SECRET;

Security Checklist

Development

  • Use environment variables for secrets
  • Enable strict TypeScript mode
  • Validate all user input
  • Use parameterized queries (automatic with ObjectQL drivers)
  • Implement proper error handling
  • Don't expose stack traces to clients

Production

  • Set OWASP security headers (secureHeaders middleware)
  • Configure CORS with explicit origin allowlist
  • Enable audit logging (34+ event types)
  • Implement session management (Better-Auth cookies)
  • Use HTTPS only
  • Enable rate limiting
  • Regular security audits
  • Keep dependencies updated
  • Enable database encryption at rest

Common Security Patterns

1. Multi-Factor Authentication

kernel.use(
  MFAPlugin({
    enabled: true,
    methods: ['totp', 'sms'],
    required_for_roles: ['admin'],
  }),
);

2. Password Policies

kernel.use(
  PasswordPolicyPlugin({
    minLength: 12,
    requireUppercase: true,
    requireLowercase: true,
    requireNumbers: true,
    requireSpecialChars: true,
    preventReuse: 5, // Can't reuse last 5 passwords
    expiryDays: 90, // Force reset every 90 days
  }),
);

3. Session Management

kernel.use(
  SessionPlugin({
    maxAge: 24 * 60 * 60 * 1000, // 24 hours
    rolling: true, // Refresh on activity
    secure: true, // HTTPS only
    httpOnly: true, // Not accessible via JavaScript
    sameSite: 'strict',
  }),
);

Testing Security

ObjectOS includes a full Auth → Permissions → Data → Audit integration test suite:

// packages/audit/test/integration.test.ts
describe('Integration: Auth → Permissions → Data → Audit', () => {
  it('should trace a full CRUD lifecycle in audit', async () => {
    // 1. Login event → recorded in audit
    await triggerHook(hooks, 'auth.login', {
      userId: 'user-1',
      userName: 'Alice',
      ipAddress: '10.0.0.1',
    });

    // 2. Permission check → passes via data.beforeCreate hook
    await triggerHook(hooks, 'data.beforeCreate', {
      objectName: 'orders',
      userId: 'user-1',
      userProfiles: ['admin'],
    });

    // 3. Data operation → recorded in audit with field changes
    await triggerHook(hooks, 'data.create', {
      objectName: 'orders',
      recordId: 'ord-1',
      userId: 'user-1',
    });

    // 4. Verify complete audit trail
    const trail = await audit.getAuditTrail('orders', 'ord-1');
    expect(trail.length).toBeGreaterThan(0);
  });
});

Performance Baseline

All CRUD operations are benchmarked to meet P95 < 100 ms:

// packages/audit/test/performance.test.ts
describe('Performance Baseline', () => {
  it('data.create pipeline P95 should be < 100 ms', async () => {
    // Runs 100 iterations through Permission + Audit pipeline
    // Measures and asserts P95 latency
  });
});

Security Resources

Edit this page on GitHub

On this page