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:
- Authentication: Verify user identity
- Authorization: Control access to objects and records
- Field-Level Security: Control access to specific fields
- Record-Level Security: Filter records based on ownership/sharing
- Audit Logging: Track all data changes
Authentication
Using Better-Auth
ObjectOS uses Better-Auth for authentication, supporting multiple strategies:
- Local (email/password)
- OAuth 2.0 (Google, GitHub, Microsoft)
- SAML (Enterprise SSO)
- LDAP
- Magic Links
Setup Authentication
import { ObjectOS } from '@objectos/kernel';
import { AuthPlugin } from '@objectos/plugin-auth';
const kernel = new ObjectOS();
// Configure auth plugin
kernel.use(AuthPlugin({
providers: {
local: {
enabled: true
},
google: {
enabled: true,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET
}
},
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '7d'
}
}));
Login Endpoint
POST /api/auth/login
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword"
}
Response:
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe",
"roles": ["sales"]
}
}
Using Tokens
Include the token in all authenticated requests:
GET /api/data/contacts
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Authorization
Object-Level Permissions
Control CRUD access to entire objects using permission sets:
# objects/contacts.object.yml
name: contacts
label: Contact
permission_set:
allowRead: true # Everyone can read
allowCreate: ['sales', 'admin'] # Only sales and admin can create
allowEdit: ['sales', 'admin'] # Only sales and admin can edit
allowDelete: ['admin'] # Only admin can delete
Permission Values:
true: Everyone has permissionfalse: No one has permission (system only)['role1', 'role2']: Only specified roles have permission
Permission Checking
ObjectOS automatically checks permissions on all operations:
// User without 'sales' or 'admin' role tries to create contact
await kernel.insert('contacts', { ... });
// Throws: PermissionDeniedError: Insufficient permissions to create contacts
Custom Permission Checks
Check permissions programmatically:
kernel.on('beforeInsert', async (ctx) => {
// Custom permission logic
if (ctx.objectName === 'opportunities' && ctx.data.amount > 100000) {
// Large opportunities require special approval
if (!ctx.user.roles.includes('senior_sales')) {
throw new Error('Large opportunities require senior sales approval');
}
}
});
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_toroles cannot see the field in queries or forms - Users without
editable_byroles cannot update the field - Attempts to read/write restricted fields return 403 Forbidden
Record-Level Security (RLS)
Filter records based on ownership or custom criteria:
Ownership-Based Access
kernel.on('beforeFind', async (ctx) => {
// Non-admin users can only see their own records
if (ctx.objectName === 'opportunities' && !ctx.user.roles.includes('admin')) {
ctx.filters = ctx.filters || {};
ctx.filters.owner = ctx.user.id;
}
});
Team-Based Access
kernel.on('beforeFind', async (ctx) => {
if (ctx.objectName === 'accounts') {
// Users can see accounts owned by their team
const userTeam = await kernel.findOne('users', ctx.user.id, {
fields: ['team_id']
});
ctx.filters = ctx.filters || {};
ctx.filters.team_id = userTeam.team_id;
}
});
Sharing Rules
Define sharing rules in metadata:
# objects/opportunities.object.yml
sharing_rules:
- name: Team Access
criteria:
team: current_user.team_id
access: read_write
- name: Manager Access
criteria:
reports_to: current_user.id
access: read_only
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
Enable audit logging to track all changes:
# objects/contacts.object.yml
name: contacts
enable_audit: true
Audit Log Structure
# objects/_audit_log.object.yml (system object)
name: _audit_log
system: true
fields:
action:
type: select
options: [create, update, delete]
object:
type: text
record_id:
type: text
user_id:
type: lookup
reference_to: users
old_values:
type: json
new_values:
type: json
timestamp:
type: datetime
ip_address:
type: text
Querying Audit Logs
// Get all changes to a record
const logs = await kernel.find('_audit_log', {
filters: {
object: 'contacts',
record_id: 'contact_123'
},
sort: [{ field: 'timestamp', order: 'desc' }]
});
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
Configure Cross-Origin Resource Sharing:
// packages/server/src/main.ts
const app = await NestFactory.create(AppModule);
app.enableCors({
origin: [
'http://localhost:5173', // Development
'https://app.example.com' // Production
],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
});
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
Set security headers in production:
app.use(helmet({
contentSecurityPolicy: {
directives: {
defaultSrc: ["'self'"],
styleSrc: ["'self'", "'unsafe-inline'"],
scriptSrc: ["'self'"],
imgSrc: ["'self'", "data:", "https:"]
}
},
hsts: {
maxAge: 31536000,
includeSubDomains: true,
preload: true
}
}));
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
- Sanitize HTML content
- Use parameterized queries (automatic with drivers)
- Implement proper error handling
- Don't expose stack traces to clients
Production
- Use HTTPS only
- Set security headers (helmet)
- Enable rate limiting
- Configure CORS properly
- Enable audit logging
- Use strong JWT secrets
- Implement session timeout
- Regular security audits
- Keep dependencies updated
- Enable database encryption at rest
- Use VPC/private networks
- Implement IP whitelisting (if needed)
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
Test security in your test suite:
describe('Security', () => {
it('should deny access without authentication', async () => {
await expect(
kernel.find('contacts', {}, null) // No user context
).rejects.toThrow('Authentication required');
});
it('should deny create without permission', async () => {
const guestUser = { id: '1', roles: ['guest'] };
await expect(
kernel.insert('contacts', { email: 'test@test.com' }, guestUser)
).rejects.toThrow('Insufficient permissions');
});
it('should filter records by ownership', async () => {
const user = { id: 'user_1', roles: ['sales'] };
const results = await kernel.find('opportunities', {}, user);
// All results should belong to user
expect(results.every(r => r.owner === user.id)).toBe(true);
});
});
Security Resources
Related Documentation
- Logic Hooks - Implement custom security logic
- SDK Reference - API reference
- HTTP Protocol - Authentication headers