API Security
This page covers authentication, authorization, and security controls for the Nexgent API.
Authentication Methods
JWT Authentication
Used by the dashboard for user sessions.
Flow:
1. User submits email/password to /api/v1/auth/login
2. Backend validates credentials (bcrypt)
3. Returns access token (15m) + refresh token (24h)
4. Client includes token in requests: Authorization: Bearer <token>
5. Middleware validates token on each requestToken Structure:
interface JWTPayload {
userId: string; // User ID
email: string; // User email
type: 'access' | 'refresh';
jti: string; // Unique token ID (for blacklisting)
iat: number; // Issued at
exp: number; // Expiration
}Middleware:
// Protect routes with authenticate middleware
router.get('/agents', authenticate, listAgents);
router.post('/agents', authenticate, createAgent);
// Optional auth for routes that work with or without auth
router.get('/public', optionalAuth, publicHandler);API Key Authentication
Used for programmatic access and integrations.
Format:
nex_<32_random_characters>
Example: nex_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6Usage:
# X-API-Key header (preferred)
curl -H "X-API-Key: nex_xxxxx" https://api.example.com/v1/signals
# Or Bearer token format
curl -H "Authorization: Bearer nex_xxxxx" https://api.example.com/v1/signalsStorage:
- Raw key shown once at creation
- Only SHA-256 hash stored in database
- Cannot be recovered if lost
// Key generation
function generateApiKey() {
const randomPart = crypto.randomBytes(24).toString('base64url');
const key = `nex_${randomPart}`;
const hash = crypto.createHash('sha256').update(key).digest('hex');
return { key, hash };
}Authorization
Resource Scoping
All API requests are scoped to the authenticated user:
// Every query filters by user ID
const agents = await prisma.agent.findMany({
where: {
userId: req.user.id, // Always filter by authenticated user
},
});This prevents:
- Accessing other users' agents
- Viewing other users' transactions
- Modifying other users' wallets
API Key Scopes
| Scope | Description | Endpoints |
|---|---|---|
full_access | Full API access (all permissions) | All endpoints |
signals | Read & write trading signals | GET/POST /signals |
agents | Read agent data & configuration | GET /agents |
positions | Read open positions | GET /positions |
balances | Read agent balances | GET /balances |
transactions | Read transaction history | GET /transactions |
history | Read historical swaps | GET /history |
Use Full Access for complete API access, or Restricted mode in the UI to select specific scopes.
Scope Enforcement:
// Require specific scope
router.post(
'/signals',
authenticateWithScope('signals'),
createSignal
);
// Check scope in middleware
if (!hasScope(keyData.scopes, requiredScope)) {
return res.status(403).json({
error: `API key missing required scope: ${requiredScope}`,
});
}Token Security
Access Token Lifecycle
Login
│
├─► Access token issued (15m TTL)
│
├─► Token used for API requests
│
├─► Token expires OR user logs out
│
└─► Token blacklisted (if logout)Token Blacklisting
On logout, access tokens are immediately invalidated:
// Logout handler
async function logout(req, res) {
const token = extractToken(req);
const payload = verifyToken(token);
// Calculate remaining TTL
const ttl = payload.exp - Math.floor(Date.now() / 1000);
// Blacklist in Redis (auto-expires when token would)
await redisTokenService.blacklistAccessToken(payload.jti, ttl);
res.json({ message: 'Logged out' });
}
// Auth middleware checks blacklist
if (await redisTokenService.isAccessTokenBlacklisted(jti)) {
return res.status(401).json({ error: 'Token has been revoked' });
}Refresh Token Rotation
Refresh tokens are single-use to prevent replay attacks:
// Refresh endpoint
async function refresh(req, res) {
const { refreshToken } = req.body;
const payload = verifyToken(refreshToken);
// Consume token (atomic delete)
const userId = await redisTokenService.consumeRefreshToken(payload.jti);
if (!userId) {
// Token already used or doesn't exist
return res.status(401).json({ error: 'Invalid refresh token' });
}
// Issue new token pair
const newAccessToken = generateAccessToken(userId, email);
const newRefreshToken = generateRefreshToken(userId, email);
// Store new refresh token
await redisTokenService.storeRefreshToken(
newRefreshToken.jti,
userId,
newRefreshToken.expiresInSeconds
);
res.json({
accessToken: newAccessToken,
refreshToken: newRefreshToken.token,
});
}Input Validation
All inputs are validated with Zod schemas:
// Define schema
const CreateSignalSchema = z.object({
tokenAddress: z.string().min(32).max(44),
action: z.enum(['buy', 'sell']),
score: z.number().min(1).max(5),
source: z.string().optional(),
});
// Validate in handler
const parsed = CreateSignalSchema.safeParse(req.body);
if (!parsed.success) {
return res.status(400).json({
error: 'Validation failed',
details: parsed.error.flatten(),
});
}
// Use validated data
const { tokenAddress, action, score } = parsed.data;Error Handling
Secure Error Responses
Production errors don't leak internal details:
// Development: detailed errors
{
"error": "Database connection failed",
"stack": "Error: connect ECONNREFUSED..."
}
// Production: generic errors
{
"error": "Internal server error"
}Authentication Errors
|| Status | Error | Cause |
||--------|-------|-------|
|| 401 | Authorization header required | No auth header |
|| 401 | Invalid authorization header format | Not Bearer format |
|| 401 | Invalid token | Malformed or wrong signature |
|| 401 | Token expired | Access token expired |
|| 401 | Token has been revoked | User logged out |
|| 401 | API key required | No API key provided |
|| 401 | Invalid API key | Key not found or wrong hash |
|| 403 | API key missing required scope | Insufficient permissions |
CORS Configuration
// Production: explicit origins only
app.use(cors({
origin: process.env.CORS_ORIGIN?.split(',') || [],
credentials: true,
}));
// Development: localhost allowed
app.use(cors({
origin: ['http://localhost:3000'],
credentials: true,
}));Never use origin: '*' in production. Always specify allowed origins explicitly.
Security Headers
Recommended headers (configure at reverse proxy):
# Nginx example
add_header X-Frame-Options "DENY" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'" always;Next Steps
- Wallet Security - Private key handling
- Network Security - Infrastructure protection