Skillshub canva-security-basics
install
source · Clone the upstream repo
git clone https://github.com/ComeOnOliver/skillshub
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/ComeOnOliver/skillshub "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/jeremylongshore/claude-code-plugins-plus-skills/canva-security-basics" ~/.claude/skills/comeonoliver-skillshub-canva-security-basics && rm -rf "$T"
manifest:
skills/jeremylongshore/claude-code-plugins-plus-skills/canva-security-basics/SKILL.mdsource content
Canva Security Basics
Overview
Security best practices for Canva Connect API OAuth 2.0 tokens, client credentials, and webhook verification. The Canva API uses OAuth with PKCE — there are no static API keys.
Token Security
Never Expose Client Secrets
# .env (NEVER commit) CANVA_CLIENT_ID=OCAxxxxxxxxxxxxxxxx CANVA_CLIENT_SECRET=xxxxxxxxxxxxxxxx # .gitignore — mandatory entries .env .env.local .env.*.local
// WRONG — client-side JavaScript can't safely hold secrets // Token exchange and refresh MUST happen server-side // "Requests that require authenticating with your client ID and // client secret can't be made from a web-browser client" — Canva docs
Token Storage
// Store tokens encrypted at rest — they grant access to user's Canva account interface SecureTokenStore { save(userId: string, tokens: { accessToken: string; // Valid ~4 hours refreshToken: string; // Single-use — always save the latest expiresAt: number; }): Promise<void>; get(userId: string): Promise<CanvaTokens | null>; delete(userId: string): Promise<void>; } // Production: use your database with encryption // Never store tokens in: localStorage, cookies without httpOnly, log files, git
Token Revocation
// Revoke tokens when user disconnects your integration async function revokeCanvaToken(token: string, clientId: string, clientSecret: string) { const basicAuth = Buffer.from(`${clientId}:${clientSecret}`).toString('base64'); await fetch('https://api.canva.com/rest/v1/oauth/revoke', { method: 'POST', headers: { 'Authorization': `Basic ${basicAuth}`, 'Content-Type': 'application/x-www-form-urlencoded', }, body: new URLSearchParams({ token }), }); }
Least-Privilege Scopes
// Request ONLY the scopes you need — scopes don't cascade // e.g., asset:write does NOT grant asset:read const SCOPE_PROFILES = { // Read-only integration — view designs and templates readonly: ['design:meta:read', 'brandtemplate:meta:read', 'folder:read'], // Content creation — create and export designs creator: ['design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read'], // Full collaboration — includes comments and webhooks collaborator: [ 'design:content:write', 'design:content:read', 'design:meta:read', 'asset:write', 'asset:read', 'comment:read', 'comment:write', 'collaboration:event', ], };
Webhook Signature Verification
Canva signs webhook payloads with JWK. Verify before processing.
import { createRemoteJWKSet, jwtVerify } from 'jose'; // Fetch Canva's public keys for webhook verification // GET https://api.canva.com/rest/v1/connect/keys const JWKS = createRemoteJWKSet( new URL('https://api.canva.com/rest/v1/connect/keys') ); async function verifyCanvaWebhook( token: string, // JWT from Canva webhook ): Promise<{ valid: boolean; payload?: any }> { try { const { payload } = await jwtVerify(token, JWKS, { issuer: 'canva', }); return { valid: true, payload }; } catch { return { valid: false }; } } // Express middleware app.post('/webhooks/canva', express.text({ type: '*/*' }), async (req, res) => { const result = await verifyCanvaWebhook(req.body); if (!result.valid) return res.status(401).send('Invalid signature'); await handleWebhookEvent(result.payload); res.status(200).send('OK'); // Must return 200 to acknowledge });
Security Checklist
- Client secret stored in environment variables / secret manager
-
files in.env.gitignore - Token exchange and refresh happen server-side only
- Access tokens encrypted at rest in database
- Refresh tokens treated as single-use (always store latest)
- Scopes follow least-privilege principle
- Webhook signatures verified with JWK
- Token revocation implemented for user disconnect
- No tokens in log output
- HTTPS enforced for all callback URLs
Error Handling
| Security Issue | Detection | Mitigation |
|---|---|---|
| Token in logs | Log audit | Redact before logging |
| Excessive scopes | Scope audit | Reduce to minimum needed |
| Stale refresh token | Auth failures | Re-authorize user |
| Unsigned webhook | Missing verification | Always verify JWK signature |
| Client secret in frontend | Code review | Server-side only |
Resources
Next Steps
For production deployment, see
canva-prod-checklist.