OS Trading Engine
Security
API Security

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 request

Token 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_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Usage:

# 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/signals

Storage:

  • 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

ScopeDescriptionEndpoints
full_accessFull API access (all permissions)All endpoints
signalsRead & write trading signalsGET/POST /signals
agentsRead agent data & configurationGET /agents
positionsRead open positionsGET /positions
balancesRead agent balancesGET /balances
transactionsRead transaction historyGET /transactions
historyRead historical swapsGET /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