Skip to main content

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 permissions
  • ADMIN - Full system access
  • API - 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):

  1. Request body ldapConfig parameter
  2. Database settings (key: 'ldap')
  3. 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

VariableDescriptionDefault
JWT_SECRETSecret for signing JWT tokensRandom (generated)
JWT_EXPIRES_INToken expiration time7d
LDAP_URLLDAP server URL-
LDAP_BASE_DNLDAP base DN-
LDAP_USER_FILTERLDAP user filter(uid={username})

Troubleshooting

"Invalid token" errors

Cause: Token expired or invalid secret.

Solution:

  • Check JWT_SECRET is consistent across deployments
  • Verify token hasn't expired
  • Check Authorization header format: Bearer <token>

Permission denied errors

Cause: User lacks required permission.

Solution:

  1. Check user's roles: GET /api/v1/auth/me
  2. Verify role permissions: GET /api/v1/roles
  3. Update roles blueprint if needed
  4. Regenerate after blueprint changes

LDAP authentication failing

Cause: Incorrect LDAP configuration.

Solution:

  1. Test LDAP connection: GET /api/v1/auth/test-ldap
  2. Verify LDAP_URL, LDAP_BASE_DN environment variables
  3. Check user filter pattern matches your LDAP schema
  4. Review LDAP server logs for connection errors

Next Steps