OAuth Redirect Broker

Some providers will not register your PeckBoard instance’s own callback URL. Slack is the clearest case: an app’s Redirect URLs must be HTTPS, must match or prefix the redirect_uri sent on the authorize request, and may contain no wildcards and no arbitrary ports. One registered app therefore cannot serve installs that each live at a different origin — which is why Cursor and Claude Code each registered their own Slack app with their own fixed redirect (cursor://… and http://localhost:3118 respectively).

A redirect broker is the way out: one fixed public callback that every install shares. You register that URL with the provider once, and each PeckBoard pulls its authorization code back from it.

What it changes

  Without a broker With a broker
redirect_uri sent {your origin}/oauth/callback {broker}/callback
Where the code lands this instance the broker, briefly
How the sign-in finishes the browser redirect PeckBoard polls {broker}/claim
What the provider must allow-list every install’s origin one URL

The broker never sees a token. It holds a single-use authorization code for about two minutes; the exchange happens on your host, with the PKCE verifier that never left the process.

Wiring it up

Settings → MCP Servers → the server’s Sign in panel → Public callback broker. Registry entries can ship it too, as oauth.redirect_broker:

"oauth": {
  "authorize_url": "https://slack.com/oauth/v2_user/authorize",
  "token_url": "https://slack.com/api/oauth.v2.user.access",
  "redirect_broker": "https://oauth.example.com/slack"
}

PeckBoard refuses a broker that is not https:// (bare http://localhost is allowed for development). Register {broker}/callback with the provider — the panel shows the exact URL once the field is set.

The contract

Two routes, no state beyond the in-flight code.

GET {broker}/callback?code=…&state=… — the provider’s redirect. Store the code (or the provider’s error) under state with a short TTL, then render a “you can close this tab” page.

GET {broker}/claim?state=… — PeckBoard’s poll.

  • holding a code → 200 {"code": "…"}, then forget it (single use)
  • provider denied → 200 {"error": "access_denied"}
  • nothing yet → 404 or 204 (PeckBoard reads this as “keep waiting”)

Reference implementation

A Cloudflare Worker with one KV namespace bound as CODES. Any equivalent runtime works — the whole thing is two handlers and a TTL.

const TTL_SECONDS = 120;

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    if (url.pathname.endsWith("/callback")) return callback(url, env);
    if (url.pathname.endsWith("/claim")) return claim(url, env);
    return new Response("not found", { status: 404 });
  },
};

// The provider's browser redirect lands here.
async function callback(url, env) {
  const state = url.searchParams.get("state") ?? "";
  // States are 32 random bytes, base64url. Anything else is not ours.
  if (state.length < 40 || state.length > 128) {
    return page(
      "Sign-in failed",
      "This redirect is missing a usable state parameter.",
    );
  }
  const error = url.searchParams.get("error");
  const code = url.searchParams.get("code");
  if (!error && !code) {
    return page(
      "Sign-in failed",
      "The redirect carried no authorization code.",
    );
  }
  await env.CODES.put(state, JSON.stringify(error ? { error } : { code }), {
    expirationTtl: TTL_SECONDS,
  });
  return page("Signed in", "You can close this tab and return to PeckBoard.");
}

// PeckBoard polls here while the consent tab is open.
async function claim(url, env) {
  const state = url.searchParams.get("state") ?? "";
  if (!state) return new Response(null, { status: 404 });
  const held = await env.CODES.get(state);
  if (!held) return new Response(null, { status: 404 });
  await env.CODES.delete(state); // single use
  return new Response(held, {
    status: 200,
    headers: { "content-type": "application/json" },
  });
}

function page(title, detail) {
  return new Response(
    `<!doctype html><meta charset="utf-8">` +
      `<title>${title}</title>` +
      `<body style="font-family:system-ui,sans-serif;text-align:center;margin-top:15vh">` +
      `<h2>${title}</h2><p>${detail}</p></body>`,
    { status: 200, headers: { "content-type": "text/html; charset=utf-8" } },
  );
}

What the broker is trusted with

Only a live authorization code, for at most the TTL, handed out once.

That code is bound to a PKCE code_verifier that never leaves your PeckBoard, and — for a confidential client — to a client secret the broker also never sees. So a leaked code alone does not mint a token. The state that keys it is 32 bytes of CSPRNG output, so codes cannot be claimed by guessing.

It is still a live credential in transit: run the broker on a host you control, over HTTPS, and keep the TTL short. Deliberately, the broker does not redirect back to the instance — a broker that forwarded codes to an origin named in the request would be an open redirect pointed at your authorization codes.


Assembled with many small pecks.

This site uses Just the Docs, a documentation theme for Jekyll.