Authentication & Authorization
Radish CLI generates a complete authentication and authorization system with role-based permissions, API key support, and framework integration.
Overview
The generated system provides:
- Authentication - User login, registration, password management
- Authorization - Role-based permissions with fine-grained control
- API Keys - Programmatic access with scoped permissions
- Session Management - JWT tokens with secure handling
- Framework Integration - Ready-to-use helpers for SvelteKit and Fastify
Core Concepts
Authentication vs Authorization
- Authentication - Who is the user? (login, sessions, API keys)
- Authorization - What can they do? (roles, permissions, ownership)
Permission Format
Permissions follow the pattern: entity:action[:scope]
Basic Permissions:
user:view:all - View all users
user:view:own - View own user record
concept:create - Create concepts
system:admin - Full system access (wildcard)
Field-Conditional Permissions:
Fine-grained access control based on field values:
comment:view:private:true:all - View private comments (all users)
post:edit:status:draft:own - Edit draft posts (own only)
blog:view:visibility:team:all - View team-visible blogs
Format: entity:action:field:value[:scope]
Role Architecture
Builtin Roles - Fast, no DB queries, defined in code:
USER- Default user role with own-scoped permissionsADMIN- Full system accessAPI- API key role
Custom Roles - Flexible, defined in roles blueprint:
- Stored in database
- Cached for performance
- Defined in
{app}.roles.yml
Generated Endpoints
Authentication
Register User
POST /api/v1/auth/register
Content-Type: application/json
{
"email": "user@example.com",
"password": "securepassword",
"displayName": "John Doe"
}
Response:
{
"user": {
"id": "...",
"email": "user@example.com",
"displayName": "John Doe"
},
"token": "eyJhbGciOi..."
}
Login
Password Login:
POST /api/v1/auth/login
Content-Type: application/json
{
"authType": "password",
"email": "user@example.com",
"password": "securepassword"
}
LDAP Login:
POST /api/v1/auth/login
Content-Type: application/json
{
"authType": "ldap",
"email": "user@company.com",
"password": "ldappassword",
"ldapConfig": {
"url": "ldap://company.com:389",
"baseDN": "ou=users,dc=company,dc=com",
"userFilter": "(uid={username})"
}
}
LDAP configuration sources (priority order):
- Request body
ldapConfigparameter - Database settings (key: 'ldap')
- Environment variables (
LDAP_URL,LDAP_BASE_DN, etc.)
Logout
POST /api/v1/auth/logout
Authorization: Bearer <token>
Change Password
POST /api/v1/auth/change-password
Authorization: Bearer <token>
Content-Type: application/json
{
"currentPassword": "oldpass",
"newPassword": "newpass"
}
User Management
Get Current User
GET /api/v1/auth/me
Authorization: Bearer <token>
List Users (Admin)
GET /api/v1/users?limit=20&cursor=...
Authorization: Bearer <admin-token>
Update User
PUT /api/v1/users/:id
Authorization: Bearer <token>
Content-Type: application/json
{
"displayName": "Updated Name"
}
Role Management
List All Roles
GET /api/v1/roles
Authorization: Bearer <admin-token>
Response:
{
"items": [
{
"id": "builtin-admin",
"key": "ADMIN",
"label": "Administrator",
"description": "Built-in administrator with all permissions",
"permissions": ["system:admin"],
"isSystem": true,
"isBuiltin": true
},
{
"id": "role-12345",
"key": "MODERATOR",
"label": "Moderator",
"permissions": ["concept:view:all", "concept:edit:all"],
"isSystem": false,
"isBuiltin": false
}
]
}
List Available Permissions
GET /api/v1/roles/available-permissions
Authorization: Bearer <admin-token>
Response:
[
{
"key": "user:view:all",
"entity": "user",
"action": "view",
"scope": "all",
"label": "View All Users",
"description": "View all user records"
}
]
Create Custom Role
POST /api/v1/roles
Authorization: Bearer <admin-token>
Content-Type: application/json
{
"key": "MODERATOR",
"label": "Moderator",
"description": "Can moderate content",
"permissions": ["user:view:all", "content:edit:all"]
}
API Keys
Create API Key
POST /api/v1/apikeys
Authorization: Bearer <token>
Content-Type: application/json
{
"name": "CI/CD Integration",
"permissions": ["user:view:all", "concept:create"],
"expiresAt": "2025-12-31T23:59:59Z"
}
Response:
{
"id": "...",
"name": "CI/CD Integration",
"token": "rk_1234567890abcdef...",
"lastEight": "...cdef1234",
"expiresAt": "2025-12-31T23:59:59Z"
}
⚠️ Important: The token is only returned once. Store it securely.
Use API Key
GET /api/v1/users
Authorization: Bearer rk_1234567890abcdef...
List API Keys
GET /api/v1/apikeys
Authorization: Bearer <token>
Revoke API Key
DELETE /api/v1/apikeys/:id
Authorization: Bearer <token>
Framework Integration
SvelteKit
The generator creates authentication helpers for SvelteKit.
Setup Hook
src/hooks.server.ts:
import { handleAuth } from '.radish/lib/datalayer/server/http/adapters/sveltekit';
export const handle = handleAuth;
This middleware:
- Validates JWT tokens
- Loads user and roles
- Attaches auth to
locals.auth - Supports API key authentication
Page Authorization
+page.server.ts:
import { hasPermission } from '.radish/lib/datalayer/server/http/adapters/sveltekit';
import { error } from '@sveltejs/kit';
export async function load({ locals }) {
// Check permission
if (!await hasPermission(locals.auth, 'admin:panel:access')) {
throw error(403, 'Forbidden');
}
// Load admin data
return {
adminData: await loadAdminData()
};
}
Component Usage
+page.svelte:
<script>
export let data;
</script>
{#if data.auth?.userId}
<p>Welcome, {data.auth.displayName}!</p>
{#if data.auth.roles?.includes('ADMIN')}
<a href="/admin">Admin Panel</a>
{/if}
{:else}
<a href="/login">Login</a>
{/if}
Form Actions
+page.server.ts:
import { UserService, createServiceAuthFromLocals } from '@generated/datalayer/services';
export const actions = {
default: async ({ locals, request }) => {
const data = await request.formData();
// Convert locals.auth to ServiceAuth format for services
const auth = createServiceAuthFromLocals(locals.auth);
const service = new UserService(auth);
await service.update(userId, {
displayName: data.get('displayName')
});
return { success: true };
}
};
Fastify
For Fastify projects:
import { authPlugin, requireAuth, requirePermission } from '.radish/lib/datalayer/server/http/fastify';
// Register auth plugin
await fastify.register(authPlugin);
// Protected route
fastify.get('/admin', {
preHandler: [requireAuth, requirePermission('admin:access')]
}, async (request, reply) => {
const user = request.user;
return { message: 'Admin area', user };
});
Service Authorization
Services automatically enforce permissions and ownership.
Permission Checks
import { ConceptService } from '@generated/datalayer/services';
// Create (requires concept:create permission)
const concept = await ConceptService.create(ctx, {
title: 'My Idea',
description: 'Description here'
});
// View all (requires concept:view:all permission)
const allConcepts = await ConceptService.find(ctx, {});
// View own (requires concept:view:own permission)
const myConcepts = await ConceptService.find(ctx, {
ownerId: ctx.userId
});
// Update (requires concept:edit:all or concept:edit:own + ownership)
await ConceptService.update(ctx, conceptId, {
title: 'Updated Title'
});
Auth Types
LocalsAuth - Type for locals.auth in SvelteKit:
interface LocalsAuth {
principal?: Principal; // User or API key principal
roles: string[]; // User's roles
scopes: string[]; // API key scopes
is: (role: string) => boolean; // Helper to check role
has: (scope: string) => boolean; // Helper to check scope
requireRole: (role: string) => void; // Throw if missing role
requireScope: (scope: string) => void; // Throw if missing scope
}
ServiceAuth - Type required by service methods:
interface ServiceAuth {
principal?: Principal;
principalType: 'user' | 'api' | 'anonymous';
userId?: string;
apiKeyId?: string;
roles: string[];
scopes: string[];
permissions: string[];
ownerId?: string;
}
Convert between them using createServiceAuthFromLocals():
import { createServiceAuthFromLocals } from '@generated/datalayer/services';
const serviceAuth = createServiceAuthFromLocals(locals.auth);
Custom Permission Checks
import { hasPermission } from '@generated/server/auth/permissions';
async function customLogic(ctx: AuthContext) {
if (await hasPermission(ctx, 'concept:publish')) {
// User can publish
}
if (await hasPermission(ctx, 'system:admin')) {
// User is admin
}
}
Defining Roles
Roles are defined in {app}.roles.yml:
version: 1
roles:
EDITOR:
label: "Content Editor"
description: "Can edit all content"
isSystem: false
permissions:
- "concept:view:all"
- "concept:edit:all"
- "concept:create"
- "user:view:all"
MODERATOR:
label: "Moderator"
description: "Can moderate user content"
isSystem: false
permissions:
- "concept:view:all"
- "concept:edit:all"
- "comment:view:all"
- "comment:delete:all"
REVIEWER:
label: "Reviewer"
description: "Can approve concepts"
isSystem: false
permissions:
- "concept:view:all"
- "concept:approve"
After modifying roles, regenerate:
radish-cli create datalayer . --schema blueprints/app.types.yml
Security Best Practices
1. Token Security
// ✅ Good - Store in httpOnly cookie
response.setHeader('Set-Cookie', `token=${jwt}; HttpOnly; Secure; SameSite=Strict`);
// ❌ Bad - Don't expose in localStorage
// localStorage.setItem('token', jwt); // Vulnerable to XSS
2. Password Requirements
// Configure in user service
const PASSWORD_MIN_LENGTH = 12;
const REQUIRE_SPECIAL_CHARS = true;
3. API Key Management
- Set expiration dates on all API keys
- Use principle of least privilege (minimal permissions)
- Rotate keys regularly
- Revoke immediately when compromised
4. Permission Scoping
# ✅ Good - Specific permissions
permissions:
- "concept:view:own"
- "concept:edit:own"
# ❌ Avoid - Overly broad
permissions:
- "system:admin" # Only for admins
5. Rate Limiting
// Add rate limiting middleware
import rateLimit from '@fastify/rate-limit';
await fastify.register(rateLimit, {
max: 100,
timeWindow: '15 minutes'
});
Environment Variables
| Variable | Description | Default |
|---|---|---|
JWT_SECRET | Secret for signing JWT tokens | Random (generated) |
JWT_EXPIRES_IN | Token expiration time | 7d |
LDAP_URL | LDAP server URL | - |
LDAP_BASE_DN | LDAP base DN | - |
LDAP_USER_FILTER | LDAP user filter | (uid={username}) |
Troubleshooting
"Invalid token" errors
Cause: Token expired or invalid secret.
Solution:
- Check
JWT_SECRETis consistent across deployments - Verify token hasn't expired
- Check Authorization header format:
Bearer <token>
Permission denied errors
Cause: User lacks required permission.
Solution:
- Check user's roles:
GET /api/v1/auth/me - Verify role permissions:
GET /api/v1/roles - Update roles blueprint if needed
- Regenerate after blueprint changes
LDAP authentication failing
Cause: Incorrect LDAP configuration.
Solution:
- Test LDAP connection:
GET /api/v1/auth/test-ldap - Verify
LDAP_URL,LDAP_BASE_DNenvironment variables - Check user filter pattern matches your LDAP schema
- Review LDAP server logs for connection errors
Next Steps
- Roles & Permissions Blueprint - Define custom roles
- Access Control - Ownership and field-level security
- Generated Services - Using authenticated services