Claude-code-plugins-plus miro-security-basics
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/miro-pack/skills/miro-security-basics" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-miro-security-basics && rm -rf "$T"
manifest:
plugins/saas-packs/miro-pack/skills/miro-security-basics/SKILL.mdsource content
Miro Security Basics
Overview
Security best practices for Miro OAuth 2.0 tokens, webhook signatures, and access control across the REST API v2.
Prerequisites
- Miro app created at https://developers.miro.com
- Understanding of OAuth 2.0 concepts
- Secret management solution for production
OAuth Token Security
Never Store Tokens in Code
# .env (NEVER commit to git) MIRO_CLIENT_ID=3458764500000001 MIRO_CLIENT_SECRET=your_client_secret_here MIRO_ACCESS_TOKEN=eyJ... MIRO_REFRESH_TOKEN=eyJ... # .gitignore — MUST include these .env .env.local .env.*.local *.pem
Scope Minimization
Request only the scopes your app actually needs. Fewer scopes = smaller blast radius if a token is compromised.
| Use Case | Minimum Scopes |
|---|---|
| Read-only dashboard | |
| Board automation | , |
| Team management | , , |
| Enterprise admin | , , |
| Full integration | , , |
Token Lifecycle Management
// src/miro/token-manager.ts interface TokenInfo { accessToken: string; refreshToken: string; expiresAt: number; // Unix timestamp in ms scopes: string[]; } class MiroTokenManager { constructor( private storage: TokenStorage, // DB, Redis, or Vault private clientId: string, private clientSecret: string, ) {} async getValidToken(userId: string): Promise<string> { const info = await this.storage.get(userId); if (!info) throw new Error('User not authorized'); // Refresh 5 minutes before expiry if (Date.now() > info.expiresAt - 300_000) { return this.refreshToken(userId, info.refreshToken); } return info.accessToken; } private async refreshToken(userId: string, refreshToken: string): Promise<string> { const response = await fetch('https://api.miro.com/v1/oauth/token', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ grant_type: 'refresh_token', client_id: this.clientId, client_secret: this.clientSecret, refresh_token: refreshToken, }), }); if (!response.ok) { // Refresh token revoked or expired — user must re-authorize await this.storage.delete(userId); throw new Error('Miro refresh token invalid. User must re-authorize.'); } const data = await response.json(); await this.storage.set(userId, { accessToken: data.access_token, refreshToken: data.refresh_token, expiresAt: Date.now() + data.expires_in * 1000, scopes: data.scope.split(' '), }); return data.access_token; } }
Webhook Signature Validation
Miro signs webhook payloads so you can verify they originate from Miro's servers.
import crypto from 'crypto'; function verifyMiroWebhookSignature( rawBody: Buffer | string, signature: string, secret: string, ): boolean { const expected = crypto .createHmac('sha256', secret) .update(rawBody) .digest('hex'); // Timing-safe comparison to prevent timing attacks try { return crypto.timingSafeEqual( Buffer.from(signature, 'hex'), Buffer.from(expected, 'hex'), ); } catch { return false; // Different lengths = not equal } } // Express middleware app.post('/webhooks/miro', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-miro-signature'] as string; if (!signature || !verifyMiroWebhookSignature(req.body, signature, process.env.MIRO_WEBHOOK_SECRET!)) { return res.status(401).json({ error: 'Invalid signature' }); } const event = JSON.parse(req.body.toString()); // Process verified event... res.status(200).json({ received: true }); } );
Client Secret Rotation
# Step 1: Generate new secret in Miro app settings # https://developers.miro.com > Your apps > Select app > App Credentials # Step 2: Update secret in your environment # For production (example with GCP Secret Manager): gcloud secrets versions add miro-client-secret \ --data-file=<(echo -n "new_secret_value") # Step 3: Verify new secret works curl -X POST https://api.miro.com/v1/oauth/token \ -d "grant_type=refresh_token" \ -d "client_id=$MIRO_CLIENT_ID" \ -d "client_secret=NEW_SECRET" \ -d "refresh_token=$MIRO_REFRESH_TOKEN" # Step 4: Revoke old secret in Miro app settings
Request Signing for Audit Trails
interface MiroAuditEntry { timestamp: string; userId: string; endpoint: string; method: string; boardId?: string; requestId?: string; // From X-Request-Id response header status: number; } async function auditedMiroFetch( userId: string, path: string, options: RequestInit = {}, ): Promise<Response> { const response = await fetch(`https://api.miro.com${path}`, { ...options, headers: { 'Authorization': `Bearer ${await tokenManager.getValidToken(userId)}`, 'Content-Type': 'application/json', ...options.headers, }, }); const audit: MiroAuditEntry = { timestamp: new Date().toISOString(), userId, endpoint: path, method: options.method ?? 'GET', boardId: path.match(/boards\/([^/]+)/)?.[1], requestId: response.headers.get('X-Request-Id') ?? undefined, status: response.status, }; // Log audit trail (never log token or request body) console.log('[MIRO_AUDIT]', JSON.stringify(audit)); return response; }
Security Checklist
- Access tokens stored in environment variables or secret manager, never in code
-
files in.env.gitignore - OAuth scopes minimized per environment
- Webhook signatures validated with timing-safe comparison
- Token refresh handled before expiry (5-minute buffer)
- Failed refresh triggers re-authorization flow
- Client secret rotation procedure documented and tested
- Audit logging captures endpoint, method, user, and status (never tokens)
-
captured for support ticket correlationX-Request-Id
Error Handling
| Security Issue | Detection | Mitigation |
|---|---|---|
| Token in logs | Log audit | Redact headers in logging middleware |
| Token in git | Pre-commit hook / secret scanning | Rotate immediately, revoke old token |
| Webhook forgery | Signature validation fails | Return 401, alert security team |
| Excessive scopes | Scope audit | Reduce to minimum needed per endpoint |
Resources
Next Steps
For production deployment, see
miro-prod-checklist.