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
-
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);
-
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 }) });
-
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);
});
});