Credential Management¶
This guide covers secure credential rotation for your partner app, including API keys, client secrets, and webhook secrets.
Overview¶
Partner apps have three types of credentials:
| Credential | Purpose | Rotation Support |
|---|---|---|
| API Key | Admin API authentication | ✅ Grace period |
| Client Secret | OAuth token exchange | ✅ Grace period |
| Webhook Secret | Webhook signature verification | ⚠️ Immediate only |
Checking Credential Health¶
Monitor your credential age and get rotation recommendations:
const status = await sdk.admin.getCredentialStatus();
console.log('API Key age:', status.api_key.age_days, 'days');
console.log('Recommended action:', status.api_key.recommended_action);
// Output: "rotate_soon" | "rotate_now" | "healthy"
Rotating API Keys¶
API keys support grace periods, allowing both old and new keys to work during transition:
const result = await sdk.admin.rotateAPIKey({
confirmation_token: 'confirm_rotation_12345',
reason: 'Monthly security rotation',
grace_period_hours: 48 // Both keys valid for 48 hours
});
// IMPORTANT: Save these values securely!
console.log('New API key:', result.new_credential);
console.log('Rollback token:', result.rollback_token);
console.log('Grace period ends:', result.grace_period_ends_at);
Grace Period
During the grace period, both old and new API keys authenticate successfully. Update your configuration before the grace period expires.
Rotating Client Secrets¶
Client secrets also support grace periods:
const result = await sdk.admin.rotateClientSecret({
confirmation_token: 'confirm_rotation_12345',
reason: 'Annual security rotation',
grace_period_hours: 168 // 7 days
});
// Update OAuth configuration with new secret
console.log('New client secret:', result.new_credential);
Rotating Webhook Secrets¶
No Grace Period
Webhook secrets rotate immediately with no grace period. Contio signs outgoing webhooks with the new secret right away.
const result = await sdk.admin.rotateWebhookSecret({
confirmation_token: 'confirm_rotation_12345',
reason: 'Quarterly security rotation'
// grace_period_hours is ignored for webhook secrets
});
Handling Webhook Secret Rotation¶
Since webhook rotation is immediate, implement dual-secret verification:
// Store both secrets during rotation window
const secrets = [process.env.WEBHOOK_SECRET_NEW, process.env.WEBHOOK_SECRET_OLD];
app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-contio-signature'] as string;
// Try each secret until one works
const isValid = secrets.some(secret =>
secret && verifyWebhookSignature(req.body, signature, secret)
);
if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process webhook...
});
Emergency Rollback¶
If issues occur after rotation, rollback within 1 hour using the rollback token:
// Rollback API key rotation
await sdk.admin.rollbackCredential('api-key', {
rollback_token: result.rollback_token
});
// Rollback client secret rotation
await sdk.admin.rollbackCredential('client-secret', {
rollback_token: result.rollback_token
});
Rollback Limitations
- Only API keys and client secrets support rollback
- Webhook secrets cannot be rolled back
- Rollback tokens expire after 1 hour
Audit History¶
View credential rotation history for compliance and debugging:
const history = await sdk.admin.getCredentialHistory({
limit: 20,
credential_type: 'api_key'
});
history.events.forEach(event => {
console.log(`${event.created_at}: ${event.action} by ${event.initiated_by}`);
});
Rate Limits¶
- Maximum 3 rotations per credential type per 24 hours
- Applies separately to each credential type
Check your rate limit status before rotating:
const history = await sdk.admin.getCredentialHistory({
limit: 100,
action: 'rotated',
});
// Count rotations in last 24 hours
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000);
const recentRotations = history.events.filter(
e => new Date(e.created_at) > oneDayAgo
);
const rotationsByType = recentRotations.reduce((acc, event) => {
acc[event.credential_type] = (acc[event.credential_type] || 0) + 1;
return acc;
}, {} as Record<string, number>);
console.log('Rotations in last 24 hours:', rotationsByType);
// { api_key: 1, webhook_secret: 0, client_secret: 0 }
Monitoring Grace Periods¶
Track credentials that are in transition:
const status = await sdk.admin.getCredentialStatus();
for (const [type, info] of Object.entries(status)) {
if (info.status === 'transitioning') {
const endsAt = new Date(info.grace_period_ends_at!);
const hoursRemaining = (endsAt.getTime() - Date.now()) / (1000 * 60 * 60);
console.log(`${type}: ${hoursRemaining.toFixed(1)} hours remaining`);
if (hoursRemaining < 4) {
console.warn('Grace period ending soon! Update your configuration.');
}
}
}
Best Practices¶
- Use grace periods - Minimum 24 hours for production
- Test in staging first - Verify new credentials work before production
- Save rollback tokens - Store securely, valid for 1 hour
- Monitor audit logs - Watch for unexpected rotations
- Rotate regularly - Every 90 days recommended
- Rotate webhook secrets during low traffic - Minimize failed deliveries
- Use webhook redelivery - Retry any deliveries that failed during rotation
Automated Rotation Workflow¶
Example workflow that checks health and rotates credentials as needed:
async function automatedRotationWorkflow(sdk: ContioPartnerSDK) {
// 1. Check credential health
const status = await sdk.admin.getCredentialStatus();
// 2. Identify credentials needing rotation
const needsRotation = Object.entries(status).filter(
([_, info]) => info.recommended_action !== 'ok'
);
if (needsRotation.length === 0) {
console.log('All credentials healthy');
return;
}
// 3. Rotate each credential
for (const [type, info] of needsRotation) {
console.log(`Rotating ${type} (${info.age_days} days old)...`);
try {
let result;
const params = {
confirmation_token: `auto_${Date.now()}`,
reason: 'Automated rotation based on age',
grace_period_hours: type === 'client_secret' ? 168 : 48,
};
if (type === 'api_key') {
result = await sdk.admin.rotateAPIKey(params);
} else if (type === 'client_secret') {
result = await sdk.admin.rotateClientSecret(params);
} else if (type === 'webhook_secret') {
result = await sdk.admin.rotateWebhookSecret(params);
}
if (result) {
console.log(`${type} rotated successfully`);
// TODO: Update your secret manager
// await updateSecretManager(type, result.new_credential);
}
} catch (error) {
console.error(`Failed to rotate ${type}:`, error);
}
}
}
Next Steps¶
- Authentication - Authentication overview
- Webhook Events - Webhook integration guide
- Troubleshooting - Common credential issues