Token Encryption via Event Subscriber
๐ฏ Overviewโ
Token encryption is now handled automatically via an event subscriber that listens to platform creation and update events. This is a cleaner, more maintainable approach than embedding encryption logic in workflows.
๐๏ธ Architectureโ
Event-Driven Encryptionโ
User creates/updates platform
โ
Platform saved to database (plaintext tokens)
โ
Event emitted: "social_platform.created" or "social_platform.updated"
โ
Subscriber triggered
โ
Subscriber checks for plaintext tokens
โ
Encrypts tokens if found
โ
Updates platform with encrypted tokens
โ
Database: Tokens now encrypted โ
Benefitsโ
โ
Separation of Concerns - Encryption logic separate from business logic
โ
Automatic - Works for all platform creation/update paths
โ
No Workflow Changes - Workflows remain simple and focused
โ
Consistent - Same encryption logic for create and update
โ
Resilient - Doesn't break platform creation if encryption fails
๐ Implementationโ
File: /src/subscribers/social-platform-credentials-encryption.tsโ
Events Listened To:
social_platform.createdsocial_platform.updated
What It Does:
- Fetches the platform by ID
- Checks
api_configfor plaintext tokens - Encrypts tokens if not already encrypted
- Updates platform with encrypted tokens
- Removes plaintext tokens
Tokens Encrypted:
access_tokenโaccess_token_encryptedrefresh_tokenโrefresh_token_encryptedoauth1_credentialsโoauth1_credentials_encryptedoauth1_app_credentialsโoauth1_app_credentials_encrypted
๐ Encryption Logicโ
Detectionโ
The subscriber only encrypts tokens that are not already encrypted:
// Check if access_token needs encryption
if (apiConfig.access_token && typeof apiConfig.access_token === 'string') {
// Only encrypt if not already encrypted
if (!apiConfig.access_token_encrypted) {
encryptedConfig.access_token_encrypted = encryptionService.encrypt(apiConfig.access_token)
delete encryptedConfig.access_token // Remove plaintext
needsUpdate = true
}
}
Encryption Processโ
- Check for plaintext - Is there a plaintext token?
- Check for encrypted - Is it already encrypted?
- Encrypt - Use
EncryptionService.encrypt() - Store encrypted - Save as
*_encryptedfield - Remove plaintext - Delete original plaintext field
- Update - Save to database
๐ก Usage Examplesโ
Example 1: Create Platform (Automatic Encryption)โ
// Create platform with plaintext token
const platform = await api.post("/admin/social-platforms", {
name: "Facebook",
category: "social",
auth_type: "oauth2",
api_config: {
access_token: "plaintext_token_12345" // Plaintext
}
})
// Subscriber automatically encrypts
// Database now has:
// api_config: {
// access_token_encrypted: { encrypted: "...", iv: "...", authTag: "..." }
// }
Example 2: Update Platform (Automatic Encryption)โ
// Update platform with new token
await api.post(`/admin/social-platforms/${platformId}`, {
api_config: {
access_token: "new_plaintext_token" // Plaintext
}
})
// Subscriber automatically encrypts the new token
Example 3: OAuth Callback (Automatic Encryption)โ
// OAuth callback stores plaintext tokens
await socials.updateSocialPlatforms([{
selector: { id: platformId },
data: {
api_config: {
access_token: oauthResponse.access_token, // Plaintext
refresh_token: oauthResponse.refresh_token // Plaintext
}
}
}])
// Subscriber automatically encrypts both tokens
๐งช Testingโ
Test Scenario 1: Platform Creationโ
// Create platform
const response = await api.post("/admin/social-platforms", {
name: "Test Platform",
api_config: {
access_token: "test_token_123"
}
})
// Fetch platform
const platform = await api.get(`/admin/social-platforms/${response.data.socialPlatform.id}`)
// Verify encryption
expect(platform.data.socialPlatform.api_config.access_token).toBeUndefined()
expect(platform.data.socialPlatform.api_config.access_token_encrypted).toBeDefined()
expect(platform.data.socialPlatform.api_config.access_token_encrypted.encrypted).toBeDefined()
Test Scenario 2: Platform Updateโ
// Update with new token
await api.post(`/admin/social-platforms/${platformId}`, {
api_config: {
access_token: "new_token_456"
}
})
// Fetch and verify
const updated = await api.get(`/admin/social-platforms/${platformId}`)
expect(updated.data.socialPlatform.api_config.access_token).toBeUndefined()
expect(updated.data.socialPlatform.api_config.access_token_encrypted).toBeDefined()
๐ Workflow Integrationโ
Before (Manual Encryption in Workflow)โ
// โ Old way - encryption in workflow
export const createSocialPlatformWorkflow = createWorkflow(
"create-social-platform",
(input) => {
const encryptedData = encryptPlatformTokensStep(input)
const result = createSocialPlatformStep(encryptedData)
return new WorkflowResponse(result)
}
)
After (Automatic via Subscriber)โ
// โ
New way - subscriber handles encryption
export const createSocialPlatformWorkflow = createWorkflow(
"create-social-platform",
(input) => {
const result = createSocialPlatformStep(input)
// Encryption happens automatically via subscriber
return new WorkflowResponse(result)
}
)
๐ก๏ธ Error Handlingโ
The subscriber is resilient and won't break platform creation:
try {
// Encrypt tokens
await socials.updateSocialPlatforms([...])
console.log("โ
Credentials encrypted")
} catch (error) {
console.error("โ Encryption failed:", error)
// Don't throw - platform is still created
// Just with plaintext tokens (which will trigger warnings)
}
Why?
- Platform creation should never fail due to encryption issues
- Plaintext tokens will trigger warnings in decrypt step
- Admin can re-save platform to trigger encryption again
๐ Monitoringโ
Console Logsโ
[Encryption Subscriber] โ Encrypted access_token for platform Facebook
[Encryption Subscriber] โ Encrypted refresh_token for platform Facebook
[Encryption Subscriber] โ
Platform Facebook credentials encrypted and saved
Error Logsโ
[Encryption Subscriber] โ Failed to encrypt credentials for platform platform_123: Error message
Metrics to Trackโ
- Number of platforms with encrypted tokens
- Number of platforms with plaintext tokens
- Encryption success rate
- Encryption failures
๐ Debuggingโ
Check if Tokens are Encryptedโ
const [platform] = await socials.listSocialPlatforms({ id: platformId })
const apiConfig = platform.api_config
console.log("Has plaintext token:", !!apiConfig.access_token)
console.log("Has encrypted token:", !!apiConfig.access_token_encrypted)
Force Re-encryptionโ
// Update platform to trigger subscriber
await socials.updateSocialPlatforms([{
selector: { id: platformId },
data: {
api_config: {
...platform.api_config,
access_token: "new_token" // Triggers encryption
}
}
}])
๐ Deploymentโ
Prerequisitesโ
- Encryption Service must be configured
- ENCRYPTION_KEY environment variable must be set
- Subscriber must be registered (automatic in MedusaJS)
Migration for Existing Platformsโ
If you have existing platforms with plaintext tokens:
// Migration script
const platforms = await socials.listSocialPlatforms({})
for (const platform of platforms) {
if (platform.api_config?.access_token) {
// Update to trigger subscriber
await socials.updateSocialPlatforms([{
selector: { id: platform.id },
data: { updated_at: new Date() }
}])
}
}
๐ Best Practicesโ
- Always use plaintext in API calls - Let subscriber handle encryption
- Never store encrypted tokens manually - Subscriber does this
- Check logs - Verify encryption is happening
- Monitor failures - Set up alerts for encryption errors
- Test thoroughly - Verify encryption in all scenarios
๐ Comparison: Workflow vs Subscriberโ
| Aspect | Workflow Approach | Subscriber Approach |
|---|---|---|
| Separation of Concerns | โ Mixed | โ Separated |
| Code Complexity | โ Higher | โ Lower |
| Maintainability | โ Harder | โ Easier |
| Consistency | โ Must remember | โ Automatic |
| Error Handling | โ Breaks workflow | โ Resilient |
| Testing | โ More complex | โ Simpler |
| Reusability | โ Per workflow | โ All paths |
๐ Related Documentationโ
Last Updated: November 19, 2025
Version: 2.0.0 (Subscriber-based)
Status: โ
Production Ready