Agent Skills: Miro Install & Auth

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/miro-install-auth

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/miro-pack/skills/miro-install-auth

Skill Files

Browse the full folder contents for miro-install-auth.

Download Skill

Loading file tree…

plugins/saas-packs/miro-pack/skills/miro-install-auth/SKILL.md

Skill Metadata

Name
miro-install-auth
Description
|

Miro Install & Auth

Overview

Set up the official @mirohq/miro-api Node.js client and configure OAuth 2.0 authentication against the Miro REST API v2 (https://api.miro.com/v2/).

Prerequisites

  • Node.js 18+
  • A Miro account (Free, Business, or Enterprise)
  • A Miro app created at https://developers.miro.com (Your apps > Create new app)
  • Client ID, Client Secret, and OAuth redirect URI from the app settings

Instructions

Step 1: Install the Official SDK

# Official Miro Node.js client
npm install @mirohq/miro-api

# For Express-based OAuth callback server
npm install express dotenv

Step 2: Configure OAuth 2.0 Credentials

# .env (NEVER commit — add to .gitignore)
MIRO_CLIENT_ID=your_client_id
MIRO_CLIENT_SECRET=your_client_secret
MIRO_REDIRECT_URI=http://localhost:3000/auth/miro/callback
MIRO_ACCESS_TOKEN=              # Filled after OAuth flow
MIRO_REFRESH_TOKEN=             # Filled after OAuth flow

Miro uses standard OAuth 2.0 authorization code flow. Tokens expire in 3599 seconds (approximately 1 hour). Always store and use the refresh token.

Step 3: OAuth 2.0 Authorization Flow

// src/auth.ts
import { Miro } from '@mirohq/miro-api';
import express from 'express';

// High-level client handles token management
const miro = new Miro({
  clientId: process.env.MIRO_CLIENT_ID!,
  clientSecret: process.env.MIRO_CLIENT_SECRET!,
  redirectUrl: process.env.MIRO_REDIRECT_URI!,
  // Storage adapter for tokens (implement for production)
  storage: {
    async get(userId: string) {
      // Return stored token for user
      return getTokenFromDB(userId);
    },
    async set(userId: string, token) {
      // Persist token
      await saveTokenToDB(userId, token);
    },
  },
});

const app = express();

// Step 1: Redirect user to Miro authorization page
app.get('/auth/miro', (req, res) => {
  const authUrl = miro.getAuthUrl();
  res.redirect(authUrl);
});

// Step 2: Handle OAuth callback
app.get('/auth/miro/callback', async (req, res) => {
  const { code } = req.query;
  if (!code || typeof code !== 'string') {
    return res.status(400).send('Missing authorization code');
  }

  try {
    // Exchange code for access_token + refresh_token
    await miro.exchangeCodeForAccessToken('default-user', code);
    res.send('Miro connected successfully!');
  } catch (err) {
    console.error('Token exchange failed:', err);
    res.status(500).send('Authentication failed');
  }
});

app.listen(3000, () => console.log('OAuth server at http://localhost:3000'));

Step 4: Direct API Access (Access Token Only)

For scripts and automation where you already have an access token:

// src/client.ts
import { MiroApi } from '@mirohq/miro-api';

// Low-level stateless client — pass token directly
const api = new MiroApi(process.env.MIRO_ACCESS_TOKEN!);

// Verify connection by listing boards
async function verifyConnection() {
  const boards = await api.getBoards();
  console.log(`Connected! Found ${boards.body.data?.length ?? 0} boards`);
  return true;
}

verifyConnection().catch(console.error);

Step 5: Configure OAuth Scopes

In your Miro app settings (https://developers.miro.com), enable the scopes your app requires:

| Scope | Purpose | Required For | |-------|---------|-------------| | boards:read | Read board data, items, members | GET endpoints | | boards:write | Create/update/delete boards and items | POST/PUT/PATCH/DELETE endpoints | | team:read | Read team info and members | Team management | | team:write | Manage team membership | Team provisioning | | organizations:read | Read org structure | Enterprise features | | identity:read | Read user profile | User identification | | auditlogs:read | Read audit logs | Enterprise compliance |

Token response after successful exchange:

{
  "access_token": "eyJ...",
  "refresh_token": "eyJ...",
  "token_type": "bearer",
  "expires_in": 3599,
  "scope": "boards:read boards:write",
  "user_id": "1234567890",
  "team_id": "9876543210"
}

Error Handling

| Error | HTTP Status | Cause | Solution | |-------|-------------|-------|----------| | insufficientPermissions | 403 | Missing OAuth scope | Add required scope in app settings and re-authorize | | tokenExpired | 401 | Access token expired | Use refresh token to get new access token | | invalidGrant | 400 | Auth code already used or expired | Restart OAuth flow from the beginning | | invalidClient | 401 | Wrong client_id or client_secret | Verify credentials in Miro app settings | | ENOTFOUND api.miro.com | N/A | DNS/network failure | Check internet and firewall rules |

Token Refresh Pattern

async function refreshAccessToken(): 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: process.env.MIRO_CLIENT_ID!,
      client_secret: process.env.MIRO_CLIENT_SECRET!,
      refresh_token: process.env.MIRO_REFRESH_TOKEN!,
    }),
  });

  if (!response.ok) {
    throw new Error(`Token refresh failed: ${response.status}`);
  }

  const data = await response.json();
  // Store new tokens
  process.env.MIRO_ACCESS_TOKEN = data.access_token;
  process.env.MIRO_REFRESH_TOKEN = data.refresh_token;
  return data.access_token;
}

Resources

Next Steps

After successful auth, proceed to miro-hello-world for your first board and item operations.