Security Best Practices
Overview
Section titled “Overview”When building Crowdin apps, securing your custom API endpoints is critical to prevent unauthorized access and potential security vulnerabilities. This guide covers essential security practices for protecting your application endpoints.
The Security Problem
Section titled “The Security Problem”When you create custom endpoints using Express, they are not automatically protected. Without proper middleware, a user who has:
- Access to only one module (e.g.,
module-a) - A valid JWT token for that module
Can potentially:
- Use their JWT token to call endpoints from other modules (e.g.,
module-b,module-c) - Access data and perform actions they shouldn’t have permission to access
This happens because the JWT token itself is valid for your application, but without the moduleKey check, there’s no verification that the token is authorized for the specific endpoint being called.
Protecting Endpoints
Section titled “Protecting Endpoints”Using Middleware
Section titled “Using Middleware”The recommended approach is to use the crowdinClientMiddleware for all custom endpoints:
const crowdinModule = require('@crowdin/app-project-module');const crowdinClientMiddleware = require('@crowdin/app-project-module/out/middlewares/crowdin-client').default;const jsonResponseMiddleware = require('@crowdin/app-project-module/out/middlewares/json-response').default;
const app = crowdinModule.express();
const configuration = { baseUrl: process.env.BASE_URL, clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, name: 'My App', identifier: 'my-app', // ... other configuration};
// Create JWT middleware with proper moduleKey configurationconst jwtMiddleware = crowdinClientMiddleware({ config: configuration, optional: false, checkSubscriptionExpiration: false, moduleKey: ['my-ai-provider', 'my-settings-menu'], // Only these modules can access this endpoint});
// Apply middleware to your custom routeapp.use('/api/my-endpoint', jwtMiddleware, jsonResponseMiddleware, myRouter);Using establishCrowdinConnection
Section titled “Using establishCrowdinConnection”If you’re using the addCrowdinEndpoints method, you can use establishCrowdinConnection with moduleKey:
const crowdinModule = require('@crowdin/app-project-module');const app = crowdinModule.express();
const configuration = { // ... your configuration};
const crowdinApp = crowdinModule.addCrowdinEndpoints(app, configuration);
app.get('/api/custom-action', async (req, res) => { try { // Establish connection with moduleKey validation const { client, context } = await crowdinApp.establishCrowdinConnection( req, ['my-module-key'] // Only this module can call this endpoint );
// Your endpoint logic here const result = await performCustomAction(client, context);
res.status(200).send({ success: true, data: result }); } catch (e) { return res.status(403).send({ error: 'Access denied' }); }});Module Key Configuration
Section titled “Module Key Configuration”Understanding Module Keys
Section titled “Understanding Module Keys”The moduleKey parameter specifies which modules are authorized to call a specific endpoint. When a request comes in:
- The JWT token is validated
- The module that made the request is extracted from the JWT payload (
jwtPayload.module) - This module is checked against the allowed
moduleKeylist - If the module is not in the list, the request is rejected with 403 error
Single Module
Section titled “Single Module”Allow only one specific module to access the endpoint:
const jwtMiddleware = crowdinClientMiddleware({ config: configuration, moduleKey: 'my-specific-module', // String for single module // ... other options});Multiple Modules
Section titled “Multiple Modules”Allow multiple modules to access the same endpoint:
const jwtMiddleware = crowdinClientMiddleware({ config: configuration, moduleKey: ['module-a', 'module-b', 'module-c'], // Array for multiple modules // ... other options});No Module Key (Insecure - Not Recommended)
Section titled “No Module Key (Insecure - Not Recommended)”const jwtMiddleware = crowdinClientMiddleware({ config: configuration, moduleKey: undefined, // ⚠️ Accepts any module - use with extreme caution // ... other options});Middleware Configuration Options
Section titled “Middleware Configuration Options”The crowdinClientMiddleware accepts the following configuration:
| Option | Type | Default | Description |
|---|---|---|---|
config | Config | - | Your application configuration object (required) |
optional | boolean | false | If true, allows requests without credentials (use for optional auth) |
checkSubscriptionExpiration | boolean | true | If true, checks if the organization’s subscription is active |
moduleKey | string | string[] | undefined | - | Module(s) authorized to access this endpoint |
Example: Optional Authentication
Section titled “Example: Optional Authentication”For endpoints that work with or without authentication:
const optionalJwtMiddleware = crowdinClientMiddleware({ config: configuration, optional: true, // Allows unauthenticated requests moduleKey: ['my-module'],});
app.get('/api/public-or-private', optionalJwtMiddleware, (req, res) => { if (req.crowdinContext) { // User is authenticated - provide personalized data return res.json({ data: getPrivateData(req.crowdinContext) }); } else { // User is not authenticated - provide public data return res.json({ data: getPublicData() }); }});Example: Subscription Checks
Section titled “Example: Subscription Checks”For paid apps, you might want to verify the subscription status:
const paidFeatureMiddleware = crowdinClientMiddleware({ config: configuration, checkSubscriptionExpiration: true, // Verify active subscription moduleKey: ['premium-feature-module'],});
app.post('/api/premium-action', paidFeatureMiddleware, (req, res) => { // This will only be reached if subscription is active // req.subscriptionInfo contains subscription details performPremiumAction(req.crowdinApiClient); res.json({ success: true });});Real-World Example
Section titled “Real-World Example”Here’s a complete example for an AI provider application with multiple endpoints:
const crowdinModule = require('@crowdin/app-project-module');const crowdinClientMiddleware = require('@crowdin/app-project-module/out/middlewares/crowdin-client').default;const jsonResponseMiddleware = require('@crowdin/app-project-module/out/middlewares/json-response').default;
const app = crowdinModule.express();
const configuration = { name: 'AI Translation Provider', identifier: 'ai-translation-provider', baseUrl: process.env.BASE_URL, clientId: process.env.CLIENT_ID, clientSecret: process.env.CLIENT_SECRET, // ... other configuration
aiProvider: { key: 'my-ai-provider', name: 'My AI Provider', // ... AI provider configuration }};
// Middleware for AI provider endpointsconst aiProviderJwtMiddleware = crowdinClientMiddleware({ config: configuration, optional: false, checkSubscriptionExpiration: true, moduleKey: ['my-ai-provider'], // Only AI provider module can access});
// Middleware for settings endpoints (accessible from multiple modules)const settingsJwtMiddleware = crowdinClientMiddleware({ config: configuration, optional: false, checkSubscriptionExpiration: false, moduleKey: ['my-ai-provider', 'my-settings-menu'], // Multiple modules});
// Protected AI endpointsapp.post('/api/ai/translate', aiProviderJwtMiddleware, jsonResponseMiddleware, async (req, res) => { const { crowdinApiClient, crowdinContext } = req; // Only accessible from the AI provider module const translation = await performTranslation(req.body, crowdinContext); res.send({ translation }); });
// Protected settings endpointsapp.get('/api/settings', settingsJwtMiddleware, jsonResponseMiddleware, async (req, res) => { const { crowdinContext } = req; // Accessible from both AI provider and settings menu const settings = await getSettings(crowdinContext); res.send({ settings }); });
app.listen(3000, () => console.log('Secure app started'));Common Mistakes to Avoid
Section titled “Common Mistakes to Avoid”❌ No Middleware Protection
Section titled “❌ No Middleware Protection”// INSECURE - No authentication at allapp.get('/api/sensitive-data', (req, res) => { res.send({ data: getSensitiveData() });});❌ Missing moduleKey
Section titled “❌ Missing moduleKey”// INSECURE - Any module with valid JWT can accessconst jwtMiddleware = crowdinClientMiddleware({ config: configuration, // moduleKey is missing!});
app.use('/api/admin-actions', jwtMiddleware, adminRouter);❌ Incorrect moduleKey
Section titled “❌ Incorrect moduleKey”// INSECURE - Wrong module key doesn't match your actual modulesconst jwtMiddleware = crowdinClientMiddleware({ config: configuration, moduleKey: ['wrong-module-key'], // This module doesn't exist in your app});✅ Correct Implementation
Section titled “✅ Correct Implementation”// SECURE - Proper middleware with correct moduleKeyconst jwtMiddleware = crowdinClientMiddleware({ config: configuration, optional: false, checkSubscriptionExpiration: true, moduleKey: ['my-actual-module-key'], // Matches your registered module});
app.use('/api/protected', jwtMiddleware, jsonResponseMiddleware, protectedRouter);Request Context
Section titled “Request Context”When using the middleware, the following properties are added to the Express request object:
// Available properties on the request object:req.crowdinContext // JWT payload and context informationreq.crowdinApiClient // Crowdin API client instance (if credentials available)req.subscriptionInfo // Subscription details (if checkSubscriptionExpiration is true)req.logInfo // Context-aware info loggerreq.logError // Context-aware error loggerYou can use these in your endpoint handlers:
app.get('/api/my-endpoint', jwtMiddleware, (req, res) => { const { crowdinContext, crowdinApiClient, logInfo } = req;
logInfo('Processing request', { userId: crowdinContext.jwtPayload.sub });
// Use crowdinContext and crowdinApiClient // ...});Security Checklist
Section titled “Security Checklist”Before deploying your Crowdin app, verify:
- All custom endpoints use either
crowdinClientMiddlewareorestablishCrowdinConnection - Every middleware configuration includes the correct
moduleKeyparameter - Module keys match the actual module identifiers registered in your app configuration
- Sensitive endpoints have
optional: falseto require authentication - Paid features use
checkSubscriptionExpiration: true - Error handling doesn’t expose sensitive information
- HTTPS is used in production environments
- Logging is implemented for security auditing