7 min read

Manage Access Keys

This guide explains how to handle access keys securely in your Stripe integration.

Understanding Access Keys

Access keys are secure tokens that:

  • Allow subscription status verification
  • Enable customer portal access
  • Permit plan changes
  • Expire automatically
  • Can be rotated for security

Initial Access Key

Access keys are generated during checkout:

// Create checkout session
const response = await iaptic.createStripeCheckout({
  offerId: 'stripe:prod_xyz#price_xyz',
  applicationUsername: 'user123',
  successUrl: 'https://example.com/success',
  cancelUrl: 'https://example.com/cancel'
});

// Store access key securely
localStorage.setItem('stripe_access_key', response.accessKey);

Key Storage

Client-side Storage

class AccessKeyManager {
  static storeKey(subscriptionId, accessKey) {
    localStorage.setItem(
      `stripe_access_key_${subscriptionId}`,
      accessKey
    );
  }

  static getKey(subscriptionId) {
    return localStorage.getItem(
      `stripe_access_key_${subscriptionId}`
    );
  }

  static removeKey(subscriptionId) {
    localStorage.removeItem(
      `stripe_access_key_${subscriptionId}`
    );
  }
}

Server-side Storage (Optional)

// If you need to store keys server-side, encrypt them:
const encrypted = await encrypt(accessKey, process.env.ENCRYPTION_KEY);
await db.query(
  'UPDATE users SET stripe_access_key = ? WHERE id = ?',
  [encrypted, userId]
);

Key Rotation

Access keys are automatically rotated when:

  • They expire (30 days)
  • Security requires it
  • Subscription changes

Handle rotation in API responses:

const { purchases, new_access_keys } = await iaptic.getPurchases(
  'sub_xyz',
  currentAccessKey
);

if (new_access_keys) {
  // Store new keys
  Object.entries(new_access_keys).forEach(([subId, newKey]) => {
    AccessKeyManager.storeKey(subId, newKey);
  });
}

Security Best Practices

  1. Storage

    // DON'T: Store in plain text server-side
    user.accessKey = accessKey;
    
    // DO: Encrypt if server storage is needed
    user.accessKey = await encrypt(accessKey);
    
    // DO: Use secure client storage
    localStorage.setItem('stripe_access_key', accessKey);
    
  2. Transmission

    // DON'T: Send in URL
    fetch(`/api/check?access_key=${accessKey}`);
    
    // DO: Send in headers or body
    fetch('/api/check', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({ access_key: accessKey })
    });
    
  3. Validation

    // Always verify key format
    function isValidAccessKey(key) {
      return /^ak_[a-zA-Z0-9]{32}$/.test(key);
    }
    
    // Handle expired keys
    try {
      const { purchases } = await iaptic.getPurchases();
    } catch (error) {
      if (error.code === 'INVALID_ACCESS_KEY') {
        AccessKeyManager.removeKey(subscriptionId);
      }
    }
    

Error Handling

class AccessKeyError extends Error {
  constructor(message, subscriptionId) {
    super(message);
    this.name = 'AccessKeyError';
    this.subscriptionId = subscriptionId;
  }
}

async function handleAccessKeyError(error, subscriptionId) {
  if (error.code === 'INVALID_ACCESS_KEY') {
    // Clear invalid key
    AccessKeyManager.removeKey(subscriptionId);
    
    // Attempt to refresh
    try {
      const { purchases, new_access_keys } = 
        await iaptic.getPurchases(subscriptionId);
      
      if (new_access_keys?.[subscriptionId]) {
        AccessKeyManager.storeKey(
          subscriptionId,
          new_access_keys[subscriptionId]
        );
        return purchases;
      }
    } catch (refreshError) {
      throw new AccessKeyError(
        'Unable to refresh access key',
        subscriptionId
      );
    }
  }
  throw error;
}

Key Lifecycle Management

class KeyLifecycleManager {
  constructor(iaptic) {
    this.iaptic = iaptic;
    this.refreshScheduler = new RefreshScheduler();
  }

  async scheduleRefresh(subscription) {
    // Schedule refresh before expiration
    const expirationDate = new Date(subscription.expirationDate);
    const refreshDate = new Date(expirationDate.getTime() - 24*60*60*1000);
    
    this.refreshScheduler.schedule(
      subscription.purchaseId,
      refreshDate,
      async () => {
        const { new_access_keys } = await this.iaptic.getPurchases(
          subscription.purchaseId
        );
        if (new_access_keys) {
          AccessKeyManager.storeKey(
            subscription.purchaseId,
            new_access_keys[subscription.purchaseId]
          );
        }
      }
    );
  }

  async cleanup() {
    // Remove expired keys
    const keys = await AccessKeyManager.getAllKeys();
    for (const [subId, key] of Object.entries(keys)) {
      try {
        await this.iaptic.getPurchases(subId, key);
      } catch (error) {
        if (error.code === 'INVALID_ACCESS_KEY') {
          AccessKeyManager.removeKey(subId);
        }
      }
    }
  }
}

Testing Access Keys

describe('Access Key Management', () => {
  it('handles key rotation', async () => {
    // 1. Create subscription
    const response = await iaptic.createStripeCheckout({/*...*/});
    const originalKey = response.accessKey;
    
    // 2. Force key expiration
    await forceKeyExpiration(response.sessionId);
    
    // 3. Verify rotation
    const { new_access_keys } = await iaptic.getPurchases(
      response.sessionId,
      originalKey
    );
    
    expect(new_access_keys).toBeDefined();
    expect(new_access_keys[response.sessionId]).not.toBe(originalKey);
  });
});