HeyPuter/puter · error · HttpError

bad_request

bad_request

Error message

Missing `opener_state`

What it means

`POST /auth/oidc/verify-popup-return` requires an `opener_state` string in the body — a signed proof token that the popup login flow passes back to the opener window. The endpoint is unauthenticated by design (it reveals nothing the caller didn't provide), so the first check is purely structural: the field must be present and non-empty.

Source

Thrown at src/backend/controllers/oidc/OIDCController.ts:211

        // parameters.
        //
        // Unauthenticated on purpose — it reveals nothing the caller did not
        // already hand over, and a forged or expired proof yields nothing.

        router.post(
            '/auth/oidc/verify-popup-return',
            {
                subdomain: 'api',
                rateLimit: {
                    scope: 'oidc-verify-popup-return',
                    limit: 60,
                    window: 60_000,
                },
            },
            async (req: Request, res: Response) => {
                const proof = req.body?.opener_state;
                if (typeof proof !== 'string' || !proof) {
                    throw new HttpError(400, 'Missing `opener_state`', {
                        legacyCode: 'bad_request',
                    });
                }
                const decoded = this.services.oidc.verifyPopupReturn(proof);
                if (!decoded) {
                    throw new HttpError(400, 'Invalid `opener_state`', {
                        legacyCode: 'bad_request',
                    });
                }
                res.json({
                    opener_origin: decoded.opener_origin ?? null,
                    msg_id: decoded.msg_id ?? null,
                    oidc_login: decoded.oidc_login === true,
                });
            },
        );

        // -- GET /auth/oidc/providers --------------------------------

View on GitHub (pinned to 908ec23eda)

Solutions

  1. Ensure the popup callback extracts `opener_state` from the OIDC redirect and includes it in the POST body.
  2. Verify the postMessage bridge passes the full token, not a truncated value.
  3. Set `Content-Type: application/json` on the request.
  4. Log the popup URL on the client to confirm the state token is present before posting.

Example fix

// before
window.opener.postMessage({ type: 'oidc-return' }, '*');

// after
const params = new URLSearchParams(location.search);
window.opener.postMessage({
  type: 'oidc-return',
  opener_state: params.get('state'),
}, openerOrigin);
Defensive patterns

Strategy: validation

Validate before calling

// Extract opener_state from the OIDC redirect before posting
const params = new URLSearchParams(window.location.search);
const openerState = params.get('state');
if (typeof openerState !== 'string' || !openerState) {
  console.error('Missing state in OIDC redirect URL');
  return;
}
await fetch('/api/auth/oidc/verify-popup-return', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({ opener_state: openerState }),
});

Type guard

/** @param {unknown} v @returns {v is string} */
function isNonEmptyString(v) {
  return typeof v === 'string' && v.length > 0;
}

Prevention

When it happens

Trigger: The popup-return handler is invoked without `opener_state` in the body; the frontend posted an empty object; the postMessage bridge between the popup and opener dropped the token.

Common situations: A popup login flow where the callback page fails to extract the state from the URL before posting; a cross-origin postMessage where the payload was serialized incorrectly; a manual test call that omits the field.

Related errors


AI-assisted analysis of HeyPuter/puter@908ec23eda (2026-08-12). Data as JSON: /api/errors/cf9f6913444148ee. Report an issue: GitHub.