Claude-code-plugins miro-install-auth

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-install-auth" ~/.claude/skills/jeremylongshore-claude-code-plugins-miro-install-auth && rm -rf "$T"
manifest: plugins/saas-packs/miro-pack/skills/miro-install-auth/SKILL.md
source content

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:

ScopePurposeRequired For
boards:read
Read board data, items, membersGET endpoints
boards:write
Create/update/delete boards and itemsPOST/PUT/PATCH/DELETE endpoints
team:read
Read team info and membersTeam management
team:write
Manage team membershipTeam provisioning
organizations:read
Read org structureEnterprise features
identity:read
Read user profileUser identification
auditlogs:read
Read audit logsEnterprise 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

ErrorHTTP StatusCauseSolution
insufficientPermissions
403Missing OAuth scopeAdd required scope in app settings and re-authorize
tokenExpired
401Access token expiredUse refresh token to get new access token
invalidGrant
400Auth code already used or expiredRestart OAuth flow from the beginning
invalidClient
401Wrong client_id or client_secretVerify credentials in Miro app settings
ENOTFOUND api.miro.com
N/ADNS/network failureCheck 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.