Skip to content

Security Best Practices

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.

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.

The recommended approach is to use the crowdinClientMiddleware for all custom endpoints:

index.js
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 configuration
const 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 route
app.use('/api/my-endpoint', jwtMiddleware, jsonResponseMiddleware, myRouter);

If you’re using the addCrowdinEndpoints method, you can use establishCrowdinConnection with moduleKey:

index.js
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' });
}
});

The moduleKey parameter specifies which modules are authorized to call a specific endpoint. When a request comes in:

  1. The JWT token is validated
  2. The module that made the request is extracted from the JWT payload (jwtPayload.module)
  3. This module is checked against the allowed moduleKey list
  4. If the module is not in the list, the request is rejected with 403 error

Allow only one specific module to access the endpoint:

const jwtMiddleware = crowdinClientMiddleware({
config: configuration,
moduleKey: 'my-specific-module', // String for single module
// ... other options
});

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
});
Section titled “No Module Key (Insecure - Not Recommended)”
const jwtMiddleware = crowdinClientMiddleware({
config: configuration,
moduleKey: undefined, // ⚠️ Accepts any module - use with extreme caution
// ... other options
});

The crowdinClientMiddleware accepts the following configuration:

OptionTypeDefaultDescription
configConfig-Your application configuration object (required)
optionalbooleanfalseIf true, allows requests without credentials (use for optional auth)
checkSubscriptionExpirationbooleantrueIf true, checks if the organization’s subscription is active
moduleKeystring | string[] | undefined-Module(s) authorized to access this endpoint

For endpoints that work with or without authentication:

index.js
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() });
}
});

For paid apps, you might want to verify the subscription status:

index.js
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 });
});

Here’s a complete example for an AI provider application with multiple endpoints:

index.js
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 endpoints
const 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 endpoints
app.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 endpoints
app.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'));
// INSECURE - No authentication at all
app.get('/api/sensitive-data', (req, res) => {
res.send({ data: getSensitiveData() });
});
// INSECURE - Any module with valid JWT can access
const jwtMiddleware = crowdinClientMiddleware({
config: configuration,
// moduleKey is missing!
});
app.use('/api/admin-actions', jwtMiddleware, adminRouter);
// INSECURE - Wrong module key doesn't match your actual modules
const jwtMiddleware = crowdinClientMiddleware({
config: configuration,
moduleKey: ['wrong-module-key'], // This module doesn't exist in your app
});
// SECURE - Proper middleware with correct moduleKey
const jwtMiddleware = crowdinClientMiddleware({
config: configuration,
optional: false,
checkSubscriptionExpiration: true,
moduleKey: ['my-actual-module-key'], // Matches your registered module
});
app.use('/api/protected', jwtMiddleware, jsonResponseMiddleware, protectedRouter);

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 information
req.crowdinApiClient // Crowdin API client instance (if credentials available)
req.subscriptionInfo // Subscription details (if checkSubscriptionExpiration is true)
req.logInfo // Context-aware info logger
req.logError // Context-aware error logger

You 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
// ...
});

Before deploying your Crowdin app, verify:

  • All custom endpoints use either crowdinClientMiddleware or establishCrowdinConnection
  • Every middleware configuration includes the correct moduleKey parameter
  • Module keys match the actual module identifiers registered in your app configuration
  • Sensitive endpoints have optional: false to 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