godcrm/backend/middleware/auth.js
GOD CRM Release f89e074dd1
Some checks failed
CI / Lint / Typecheck / Test / Build (push) Has been cancelled
CI / PostgreSQL Integration Tests (push) Has been cancelled
GOD CRM — public scrubbed snapshot
Governed substrate for autonomous agents: scoped identity (passports),
audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
2026-08-10 04:01:45 +03:00

293 lines
8.5 KiB
JavaScript

import jwt from 'jsonwebtoken';
import crypto from 'crypto';
import { dbGet, dbRun } from '../database/connection.js';
import { authLogger } from '../utils/logger.js';
/**
* Hash API key for secure storage/comparison
*/
function hashApiKey(key) {
return crypto.createHash('sha256').update(key).digest('hex');
}
/**
* Authenticate via API key
* Supports: X-API-Key header or Authorization: Bearer sk-xxx
* Returns: { success: boolean, user?: object, error?: string, details?: object }
*/
async function authenticateApiKey(apiKey) {
// API keys start with 'sk-' prefix followed by 32 random chars
// Format: sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
if (!apiKey || !apiKey.startsWith('sk-')) {
return {
success: false,
error: 'Invalid API key format',
details: { expected: 'sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' }
};
}
const keyPrefix = apiKey.substring(0, 7); // sk-xxxx
const keyHash = hashApiKey(apiKey);
// Find API key in database
const keyRecord = await dbGet(`
SELECT ak.*, u.id as user_id, u.email, u.name, u.role
FROM api_keys ak
JOIN users u ON ak.user_id = u.id
WHERE ak.key_prefix = ? AND ak.key_hash = ?
`, [keyPrefix, keyHash]);
if (!keyRecord) {
return {
success: false,
error: 'API key not found',
details: { prefix: keyPrefix }
};
}
// Check if key is active
if (!keyRecord.is_active) {
return {
success: false,
error: 'API key is disabled',
details: { keyId: keyRecord.id, keyName: keyRecord.name }
};
}
// Check expiration
if (keyRecord.expires_at && new Date(keyRecord.expires_at) < new Date()) {
return {
success: false,
error: 'API key has expired',
details: {
keyId: keyRecord.id,
keyName: keyRecord.name,
expiredAt: keyRecord.expires_at
}
};
}
// Update usage stats (async, don't wait)
dbRun(`
UPDATE api_keys
SET last_used_at = CURRENT_TIMESTAMP, request_count = request_count + 1
WHERE id = ?
`, [keyRecord.id]).catch(err => authLogger.error({ err }, 'Failed to update API key usage stats'));
// Return user object similar to JWT decode
return {
success: true,
user: {
id: keyRecord.user_id,
email: keyRecord.email,
name: keyRecord.name,
role: keyRecord.role,
apiKeyId: keyRecord.id,
apiKeyName: keyRecord.name,
scopes: JSON.parse(keyRecord.scopes || '["*"]'),
// Access restrictions
allowedSpaces: keyRecord.allowed_spaces ? JSON.parse(keyRecord.allowed_spaces) : null,
allowedProjects: keyRecord.allowed_projects ? JSON.parse(keyRecord.allowed_projects) : null,
allowedTables: keyRecord.allowed_tables ? JSON.parse(keyRecord.allowed_tables) : null
}
};
}
export async function authenticate(req, res, next) {
// SEC-9: Removed debug logging for /tree paths
// 1. Check for API Key in X-API-Key header
const apiKeyHeader = req.headers['x-api-key'];
if (apiKeyHeader) {
const result = await authenticateApiKey(apiKeyHeader);
if (result.success) {
req.user = result.user;
req.authMethod = 'api_key';
return next();
}
return res.status(401).json({
success: false,
error: {
code: 'API_KEY_INVALID',
message: result.error || 'Invalid API key',
details: result.details || null
}
});
}
// 2. Check Authorization header (Bearer token - can be JWT or API key)
const authHeader = req.headers.authorization;
if (authHeader && authHeader.startsWith('Bearer ')) {
const token = authHeader.split(' ')[1];
// Check if it's an API key (starts with sk-)
if (token.startsWith('sk-')) {
const result = await authenticateApiKey(token);
if (result.success) {
req.user = result.user;
req.authMethod = 'api_key';
return next();
}
return res.status(401).json({
success: false,
error: {
code: 'API_KEY_INVALID',
message: result.error || 'Invalid API key',
details: result.details || null
}
});
}
// Otherwise treat as JWT
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
req.authMethod = 'jwt';
return next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
}
// 3. Check JWT token in cookies (for browser sessions)
const accessToken = req.cookies?.access_token;
if (accessToken) {
try {
const decoded = jwt.verify(accessToken, process.env.JWT_SECRET);
req.user = decoded;
req.authMethod = 'cookie';
return next();
} catch (error) {
// Token expired or invalid - let it fall through to 401
}
}
// 4. Check query param token (for SSE/EventSource which can't send headers)
const queryToken = req.query?.token;
if (queryToken) {
try {
const decoded = jwt.verify(queryToken, process.env.JWT_SECRET);
req.user = decoded;
req.authMethod = 'query_token';
return next();
} catch (error) {
// Invalid token - fall through to 401
}
}
// 5. No authentication provided
return res.status(401).json({ error: 'No authentication provided. Use X-API-Key header or Authorization: Bearer <token>' });
}
export function requireRole(role) {
return (req, res, next) => {
if (req.user.role !== role && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
/**
* Middleware to check API key scopes
* Usage: requireScope('tables:write')
*/
export function requireScope(scope) {
return (req, res, next) => {
// JWT/Cookie users have all permissions
if (req.authMethod === 'jwt' || req.authMethod === 'cookie') {
return next();
}
// Check API key scopes
const scopes = req.user.scopes || [];
if (scopes.includes('*') || scopes.includes(scope)) {
return next();
}
return res.status(403).json({
success: false,
error: {
code: 'INSUFFICIENT_SCOPE',
message: `API key does not have required permission: ${scope}`,
required: scope,
available: scopes,
keyName: req.user.apiKeyName || 'unknown'
}
});
};
}
/**
* Middleware to check access to specific resource (space/project/table)
* Usage: requireResourceAccess('table', (req) => req.params.id)
*/
export function requireResourceAccess(resourceType, getResourceId) {
return async (req, res, next) => {
// JWT/Cookie users have all permissions
if (req.authMethod === 'jwt' || req.authMethod === 'cookie') {
return next();
}
const resourceId = typeof getResourceId === 'function' ? getResourceId(req) : getResourceId;
if (!resourceId) {
return next(); // No resource to check
}
const user = req.user;
// Check based on resource type
if (resourceType === 'space') {
if (user.allowedSpaces && !user.allowedSpaces.includes(Number(resourceId))) {
return res.status(403).json({
success: false,
error: {
code: 'ACCESS_DENIED_SPACE',
message: `API key does not have access to space ${resourceId}`,
resourceType: 'space',
resourceId: resourceId,
keyName: user.apiKeyName || 'unknown',
allowedSpaces: user.allowedSpaces
}
});
}
}
if (resourceType === 'project') {
if (user.allowedProjects && !user.allowedProjects.includes(Number(resourceId))) {
return res.status(403).json({
success: false,
error: {
code: 'ACCESS_DENIED_PROJECT',
message: `API key does not have access to project ${resourceId}`,
resourceType: 'project',
resourceId: resourceId,
keyName: user.apiKeyName || 'unknown',
allowedProjects: user.allowedProjects
}
});
}
}
if (resourceType === 'table') {
if (user.allowedTables && !user.allowedTables.includes(Number(resourceId))) {
return res.status(403).json({
success: false,
error: {
code: 'ACCESS_DENIED_TABLE',
message: `API key does not have access to table ${resourceId}`,
resourceType: 'table',
resourceId: resourceId,
keyName: user.apiKeyName || 'unknown',
allowedTables: user.allowedTables
}
});
}
}
return next();
};
}
// Alias for compatibility
export const authMiddleware = authenticate;