Skip to content

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

  1. Use grace periods - Minimum 24 hours for production
  2. Test in staging first - Verify new credentials work before production
  3. Save rollback tokens - Store securely, valid for 1 hour
  4. Monitor audit logs - Watch for unexpected rotations
  5. Rotate regularly - Every 90 days recommended
  6. Rotate webhook secrets during low traffic - Minimize failed deliveries
  7. 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