| name | miro-security-basics |
| description | Apply Miro REST API v2 security best practices — OAuth scope minimization,
token storage, webhook signature validation, and secret rotation.
Trigger with phrases like "miro security", "miro secrets",
"secure miro", "miro token security", "miro webhook signature".
|
| allowed-tools | Read, Write, Grep |
| version | 1.6.0 |
| license | MIT |
| author | Jeremy Longshore <jeremy@intentsolutions.io> |
| tags | ["saas","miro","security","oauth"] |
| compatibility | Designed for Claude Code |
Miro Security Basics
Overview
Security best practices for Miro OAuth 2.0 tokens, webhook signatures, and access control across the REST API v2.
Prerequisites
OAuth Token Security
Never Store Tokens in Code
MIRO_CLIENT_ID=3458764500000001
MIRO_CLIENT_SECRET=your_client_secret_here
MIRO_ACCESS_TOKEN=eyJ...
MIRO_REFRESH_TOKEN=eyJ...
.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 | boards:read |
| Board automation | boards:read, boards:write |
| Team management | boards:read, team:read, team:write |
| Enterprise admin | boards:read, organizations:read, auditlogs:read |
| Full integration | boards:read, boards:write, identity:read |
Token Lifecycle Management
interface TokenInfo {
accessToken: string;
refreshToken: string;
expiresAt: number;
scopes: string[];
}
class MiroTokenManager {
constructor(
private storage: TokenStorage,
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');
if (Date.now() > info.expiresAt - 300_000) {
return this.refreshToken(userId, info.refreshToken);
}
info.;
}
(: , : ): <> {
response = (, {
: ,
: { : },
: ({
: ,
: .,
: .,
: refreshToken,
}),
});
(!response.) {
..(userId);
();
}
data = response.();
..(userId, {
: data.,
: data.,
: .() + data. * ,
: data..(),
});
data.;
}
}
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');
try {
return crypto.timingSafeEqual(
Buffer.from(signature, 'hex'),
Buffer.from(expected, 'hex'),
);
} catch {
return false;
}
}
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..!)) {
res.().({ : });
}
event = .(req..());
res.().({ : });
}
);
Client Secret Rotation
gcloud secrets versions add miro-client-secret \
--data-file=<(echo -n "new_secret_value")
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"
Request Signing for Audit Trails
interface MiroAuditEntry {
timestamp: string;
userId: string;
endpoint: string;
method: string;
boardId?: string;
requestId?: string;
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 ?? ,
: path.()?.[],
: response..() ?? ,
: response.,
};
.(, .(audit));
response;
}
Security Checklist
Error Handling
| Security Issue | Detection | Mitigation |
|---|
| Token in logs | Log audit | Redact Authorization 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.