Version: 1.0
Purpose: Pluggable authentication for NQL
NQL does not dictate authentication. Instead, it provides an adapter interface that allows applications to integrate their existing auth systems.
Applications have diverse authentication requirements:
- Firebase Authentication
- NextAuth.js
- Auth0
- Custom JWT
- OAuth 2.0
- Session-based
- API keys
- Multi-tenant isolation
NQL adapts to YOUR auth system rather than forcing you to change.
┌────────────────────────────────────────┐
│ HTTP Request │
│ Authorization: Bearer <token> │
└──────────────┬─────────────────────────┘
│
↓
┌────────────────────────────────────────┐
│ Your Auth Adapter │
│ ┌────────────────────────────────────┐ │
│ │ extractContext(request) │ │
│ │ → { user_id, role, ... } │ │
│ └────────────────────────────────────┘ │
└──────────────┬─────────────────────────┘
│
↓
┌────────────────────────────────────────┐
│ NQL Client │
│ Uses context for authorization & RLS │
└────────────────────────────────────────┘
Every auth adapter must implement three methods:
interface AuthAdapter {
// Extract user context from request
extractContext(request: Request): Promise<UserContext>;
// Check if operation is allowed
checkPermission(
userContext: UserContext,
operation: string,
resource: string
): Promise<boolean>;
// Apply row-level security filters
applyRLS(
nqlQuery: NQLQuery,
userContext: UserContext,
resource: string
): Promise<NQLQuery>;
}
interface UserContext {
user_id: string;
[key: string]: any; // Additional context (role, permissions, etc.)
}class SimpleJWTAdapter {
constructor(jwtSecret) {
this.jwtSecret = jwtSecret;
}
async extractContext(request) {
const token = this.extractToken(request);
if (!token) {
throw new Error('NQL_3001: Authentication required');
}
try {
const decoded = jwt.verify(token, this.jwtSecret);
return {
user_id: decoded.sub,
email: decoded.email,
role: decoded.role || 'user'
};
} catch (error) {
throw new Error('NQL_3001: Invalid token');
}
}
async checkPermission(userContext, operation, resource) {
// Simple role-based check
if (userContext.role === 'admin') {
return true; // Admins can do anything
}
// Regular users can only read
return operation === 'read';
}
async applyRLS(nqlQuery, userContext, resource) {
// Non-admins see only their own records
if (userContext.role !== 'admin') {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ user_id: { $eq: userContext.user_id } }
]
};
}
return nqlQuery;
}
extractToken(request) {
const authHeader = request.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
}
// Usage
const client = new NQLClient({
schema: './schema.yaml',
database: dbConfig,
authAdapter: new SimpleJWTAdapter(process.env.JWT_SECRET)
});const admin = require('firebase-admin');
class FirebaseAuthAdapter {
async extractContext(request) {
const token = this.extractToken(request);
if (!token) {
throw new Error('NQL_3001: Authentication required');
}
try {
const decodedToken = await admin.auth().verifyIdToken(token);
return {
user_id: decodedToken.uid,
email: decodedToken.email,
email_verified: decodedToken.email_verified,
role: decodedToken.custom_claims?.role || 'user',
permissions: decodedToken.custom_claims?.permissions || []
};
} catch (error) {
throw new Error('NQL_3001: Invalid Firebase token');
}
}
async checkPermission(userContext, operation, resource) {
// Check custom claims
const requiredPermission = `${operation}:${resource}`;
if (userContext.role === 'admin') {
return true;
}
return userContext.permissions.includes(requiredPermission);
}
async applyRLS(nqlQuery, userContext, resource) {
// Apply RLS based on resource
const rlsRules = {
'users': () => {
// Users can only access their own record
if (userContext.role !== 'admin') {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ id: { $eq: userContext.user_id } }
]
};
}
},
'orders': () => {
// Users see only their orders
if (userContext.role !== 'admin') {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ customer_id: { $eq: userContext.user_id } }
]
};
}
}
};
const rule = rlsRules[resource];
if (rule) {
rule();
}
return nqlQuery;
}
extractToken(request) {
const authHeader = request.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
}import { getSession } from 'next-auth/react';
class NextAuthAdapter {
async extractContext(request) {
const session = await getSession({ req: request });
if (!session || !session.user) {
throw new Error('NQL_3001: Not authenticated');
}
return {
user_id: session.user.id,
email: session.user.email,
role: session.user.role,
permissions: session.user.permissions || []
};
}
async checkPermission(userContext, operation, resource) {
const requiredPerm = `${operation}:${resource}`;
if (userContext.role === 'admin') {
return true;
}
return userContext.permissions.includes(requiredPerm);
}
async applyRLS(nqlQuery, userContext, resource) {
if (userContext.role !== 'admin') {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ user_id: { $eq: userContext.user_id } }
]
};
}
return nqlQuery;
}
}class MultiTenantAdapter {
constructor(sessionStore) {
this.sessionStore = sessionStore;
}
async extractContext(request) {
const sessionId = request.cookies.session_id;
const session = await this.sessionStore.get(sessionId);
if (!session) {
throw new Error('NQL_3001: Invalid session');
}
return {
user_id: session.userId,
tenant_id: session.tenantId, // Organization ID
role: session.role,
department: session.department
};
}
async checkPermission(userContext, operation, resource) {
// Tenant admins can do anything in their tenant
if (userContext.role === 'tenant_admin') {
return true;
}
// Department managers can read/create
if (userContext.role === 'dept_manager') {
return ['read', 'create'].includes(operation);
}
// Regular users can only read
return operation === 'read';
}
async applyRLS(nqlQuery, userContext, resource) {
// ALWAYS filter by tenant (critical for multi-tenant)
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ tenant_id: { $eq: userContext.tenant_id } }
]
};
// Additional filters based on role
if (userContext.role === 'dept_manager') {
// See only their department
nqlQuery.params.filter.$and.push(
{ department_id: { $eq: userContext.department } }
);
} else if (userContext.role === 'user') {
// See only own records
nqlQuery.params.filter.$and.push(
{ user_id: { $eq: userContext.user_id } }
);
}
return nqlQuery;
}
}class OAuthRBACAdapter {
constructor(oauthProvider, permissionService) {
this.oauth = oauthProvider;
this.permissionService = permissionService;
}
async extractContext(request) {
const token = this.extractToken(request);
if (!token) {
throw new Error('NQL_3001: Authentication required');
}
// Verify with OAuth provider
const userInfo = await this.oauth.getUserInfo(token);
// Fetch roles from your database
const roles = await this.permissionService.getUserRoles(userInfo.sub);
return {
user_id: userInfo.sub,
email: userInfo.email,
roles: roles,
scopes: token.scope.split(' ')
};
}
async checkPermission(userContext, operation, resource) {
// Check OAuth scopes
const requiredScope = `${operation}:${resource}`;
if (!userContext.scopes.includes(requiredScope)) {
return false;
}
// Check RBAC roles
const allowedRoles = await this.permissionService
.getRolesForOperation(operation, resource);
return userContext.roles.some(role => allowedRoles.includes(role));
}
async applyRLS(nqlQuery, userContext, resource) {
const hasAdminRole = userContext.roles.includes('admin');
if (!hasAdminRole) {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ user_id: { $eq: userContext.user_id } }
]
};
}
return nqlQuery;
}
extractToken(request) {
const authHeader = request.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
}class CustomSessionAdapter {
constructor(sessionStore, db) {
this.sessionStore = sessionStore;
this.db = db;
}
async extractContext(request) {
const sessionId = request.cookies.session_id;
const session = await this.sessionStore.get(sessionId);
if (!session) {
throw new Error('NQL_3001: Invalid session');
}
// Fetch additional context from database
const userDetails = await this.db.query(`
SELECT
u.id, u.role,
d.id as department_id,
d.manager_id
FROM users u
LEFT JOIN departments d ON u.department_id = d.id
WHERE u.id = $1
`, [session.userId]);
return {
user_id: session.userId,
role: userDetails.role,
department_id: userDetails.department_id,
is_manager: userDetails.manager_id === session.userId
};
}
async checkPermission(userContext, operation, resource) {
// Custom permission logic
if (userContext.role === 'admin') {
return true;
}
if (userContext.is_manager) {
return ['read', 'create', 'update'].includes(operation);
}
return operation === 'read';
}
async applyRLS(nqlQuery, userContext, resource) {
if (userContext.role === 'admin') {
return nqlQuery; // No filtering for admins
}
if (userContext.is_manager) {
// Managers see their department
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ department_id: { $eq: userContext.department_id } }
]
};
} else {
// Regular users see only own records
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ user_id: { $eq: userContext.user_id } }
]
};
}
return nqlQuery;
}
}class BaseAuthAdapter {
async extractContext(request) {
throw new Error('extractContext must be implemented');
}
async checkPermission(userContext, operation, resource) {
throw new Error('checkPermission must be implemented');
}
async applyRLS(nqlQuery, userContext, resource) {
// Default: no RLS filtering
return nqlQuery;
}
// Helper methods
extractToken(request) {
const authHeader = request.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
return authHeader.substring(7);
}
return null;
}
isAdmin(userContext) {
return userContext.role === 'admin' ||
userContext.roles?.includes('admin');
}
hasPermission(userContext, permission) {
return userContext.permissions?.includes(permission);
}
injectFilter(nqlQuery, field, value) {
nqlQuery.params.filter = {
$and: [
nqlQuery.params.filter || {},
{ [field]: { $eq: value } }
]
};
return nqlQuery;
}
}class NoAuthAdapter {
async extractContext(request) {
return {
user_id: 'anonymous',
role: 'public'
};
}
async checkPermission() {
return true; // Allow everything
}
async applyRLS(nqlQuery) {
return nqlQuery; // No filtering
}
}
// Use for testing
const client = new NQLClient({
schema: './schema.yaml',
database: dbConfig,
authAdapter: new NoAuthAdapter()
});class MockAuthAdapter {
constructor(mockUser) {
this.mockUser = mockUser;
}
async extractContext(request) {
return this.mockUser;
}
async checkPermission() {
return true;
}
async applyRLS(nqlQuery) {
return nqlQuery;
}
}
// In tests
const mockUser = { user_id: 'test-123', role: 'admin' };
const adapter = new MockAuthAdapter(mockUser);// server.js
const express = require('express');
const { NQLClient } = require('@nql/core');
const { FirebaseAuthAdapter } = require('./auth/firebase');
const app = express();
const nqlClient = new NQLClient({
schema: './schema.yaml',
database: {
type: 'postgresql',
connection: process.env.DATABASE_URL
},
authAdapter: new FirebaseAuthAdapter()
});
app.post('/api/query', async (req, res) => {
try {
const result = await nqlClient.execute(
req.body.query,
req // Pass request for auth extraction
);
res.json(result);
} catch (error) {
if (error.message.startsWith('NQL_3001')) {
res.status(401).json({ error: 'Unauthorized' });
} else {
res.status(500).json({ error: error.message });
}
}
});- Extract minimal context: Only extract what you need
- Cache user data: Don't fetch from DB on every request
- Fail secure: Default to deny if permission check fails
- Log auth failures: Track unauthorized access attempts
- Use strong typing: TypeScript helps catch auth bugs
- Test thoroughly: Write unit tests for auth logic
- Handle token expiry: Return appropriate error codes
- Validate input: Don't trust auth tokens blindly
- Always verify tokens: Don't trust client-provided context
- Use HTTPS: Protect tokens in transit
- Rotate secrets: Change JWT secrets periodically
- Rate limit: Prevent brute force attacks
- Log suspicious activity: Monitor for attack patterns
- Validate permissions: Check both scope and role
- Enforce RLS: Always filter by tenant/user in multi-tenant apps
Version: 1.0
Status: Draft
License: MIT
Author: nagibaba
For questions: https://github.com/nagibaba/nql