Skip to content

Authentication

The simplest way to authenticate API requests is using API keys.

  1. Navigate to the admin panel at /admin
  2. Go to Settings → API Keys
  3. Click “Generate New Key” and set permissions
  4. Copy the generated key (it won’t be shown again)

Include the API key in the Authorization header:

Terminal window
curl -H "Authorization: Bearer your-api-key" \
https://your-app.com/api/posts

Configure granular permissions for each API key:

{
"permissions": {
"models": {
"post": ["read", "create", "update"],
"user": ["read"],
"category": ["read", "create", "update", "delete"]
},
"admin": false,
"rateLimit": {
"requests": 1000,
"windowMs": 3600000
}
}
}

For user-based authentication, Scalar supports JWT tokens.

scalar.config.ts
export default defineConfig({
auth: {
jwt: {
secret: process.env.JWT_SECRET,
expiresIn: '7d',
algorithm: 'HS256',
},
providers: {
email: {
enabled: true,
smtp: {
host: process.env.SMTP_HOST,
port: 587,
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
},
github: {
enabled: true,
clientId: process.env.GITHUB_CLIENT_ID,
clientSecret: process.env.GITHUB_CLIENT_SECRET,
},
},
},
});
Terminal window
curl -X POST https://your-app.com/api/auth/signup \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"password": "securepassword",
"name": "John Doe"
}'

Response:

{
"user": {
"id": "user_123",
"email": "user@example.com",
"name": "John Doe"
},
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"expiresAt": "2024-01-22T10:00:00Z"
}
Terminal window
# Redirect to GitHub OAuth
GET https://your-app.com/api/auth/github
# Handle callback (automatic)
GET https://your-app.com/api/auth/github/callback?code=...
scalar.config.ts
export default defineConfig({
auth: {
providers: {
google: {
enabled: true,
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
scope: ['email', 'profile'],
},
},
},
});
Terminal window
curl -X POST https://your-app.com/api/auth/session \
-H "Authorization: Bearer jwt-token-here"
Terminal window
curl -H "Cookie: session=session-token" \
https://your-app.com/api/posts
scalar.config.ts
export default defineConfig({
session: {
strategy: 'jwt', // 'jwt' or 'database'
maxAge: 7 * 24 * 60 * 60, // 7 days
updateAge: 24 * 60 * 60, // 1 day
cookies: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
sameSite: 'lax',
},
},
});
schema/user.ts
export const user = defineModel({
name: 'user',
fields: {
email: { type: 'email', required: true, unique: true },
name: { type: 'text', required: true },
role: {
type: 'select',
options: ['viewer', 'editor', 'admin', 'super_admin'],
defaultValue: 'viewer',
},
permissions: {
type: 'array',
of: 'text',
label: 'Custom Permissions',
},
},
});
schema/post.ts
export const post = defineModel({
name: 'post',
permissions: {
create: ['admin', 'editor'],
read: ['admin', 'editor', 'viewer'],
update: ['admin', 'editor'],
delete: ['admin'],
// Field-level permissions
fields: {
publishedAt: {
update: ['admin'],
},
},
},
});
middleware/auth.ts
import { scalar } from '@/lib/scalar';
export async function checkPermission(
userId: string,
action: string,
resource: string,
) {
const user = await scalar.content.findOne({
model: 'user',
filter: { id: userId },
});
// Check role-based permissions
if (user.role === 'admin') return true;
// Check custom permissions
const permission = `${action}:${resource}`;
return user.permissions.includes(permission);
}
middleware.ts
import { NextRequest, NextResponse } from 'next/server';
import { verifyToken } from '@/lib/auth';
export async function middleware(request: NextRequest) {
// Check if request needs authentication
if (request.nextUrl.pathname.startsWith('/api/admin')) {
const token = request.headers.get('authorization')?.replace('Bearer ', '');
if (!token) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 },
);
}
try {
const payload = await verifyToken(token);
// Add user info to request headers
const requestHeaders = new Headers(request.headers);
requestHeaders.set('x-user-id', payload.userId);
requestHeaders.set('x-user-role', payload.role);
return NextResponse.next({
request: {
headers: requestHeaders,
},
});
} catch (error) {
return NextResponse.json({ error: 'Invalid token' }, { status: 401 });
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/api/admin/:path*', '/admin/:path*'],
};
middleware/auth.js
const jwt = require('jsonwebtoken');
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Invalid or expired token' });
}
req.user = user;
next();
});
}
module.exports = authenticateToken;
// ✅ Good - Server-side only
const token = jwt.sign(payload, process.env.JWT_SECRET);
// ❌ Bad - Client-side exposure
const token = jwt.sign(payload, 'hardcoded-secret');
scalar.config.ts
export default defineConfig({
security: {
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // requests per window
skipSuccessfulRequests: false,
standardHeaders: true,
},
bruteForce: {
freeRetries: 5,
minWait: 5 * 60 * 1000, // 5 minutes
maxWait: 15 * 60 * 1000, // 15 minutes
failuresBeforeBrute: 5,
},
},
});
scalar.config.ts
export default defineConfig({
api: {
cors: {
origin: ['http://localhost:3000', 'https://yourdomain.com'],
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
},
},
});
__tests__/auth.test.ts
import { createMocks } from 'node-mocks-http';
import { authHandler } from '@/pages/api/auth/signin';
describe('/api/auth/signin', () => {
it('should authenticate valid credentials', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
email: 'test@example.com',
password: 'testpassword',
},
});
await authHandler(req, res);
expect(res._getStatusCode()).toBe(200);
const data = JSON.parse(res._getData());
expect(data).toHaveProperty('token');
expect(data).toHaveProperty('user');
});
it('should reject invalid credentials', async () => {
const { req, res } = createMocks({
method: 'POST',
body: {
email: 'test@example.com',
password: 'wrongpassword',
},
});
await authHandler(req, res);
expect(res._getStatusCode()).toBe(401);
});
});
__tests__/protected-routes.test.ts
import request from 'supertest';
import app from '@/app';
describe('Protected Routes', () => {
let authToken: string;
beforeAll(async () => {
// Get auth token
const response = await request(app).post('/api/auth/signin').send({
email: 'admin@example.com',
password: 'adminpassword',
});
authToken = response.body.token;
});
it('should access protected route with valid token', async () => {
const response = await request(app)
.get('/api/admin/users')
.set('Authorization', `Bearer ${authToken}`);
expect(response.status).toBe(200);
});
it('should reject access without token', async () => {
const response = await request(app).get('/api/admin/users');
expect(response.status).toBe(401);
});
});