# Restrict app access

Block direct browser launches and protect your app's data and actions with verified Zetapad access.

Source: https://zetapad.com/developers/docs/access-control

## Required behavior

If your app is available only through Zetapad, opening its URL directly in a browser must display a blocked page rather than a usable app. A permitted iframe initially receives only a public bootstrap shell. The app becomes usable after a trusted parent handshake and server-verified Zetapad authentication.

Your hosted app enforces this policy. Registering a URL in Zetapad does not change your hosting provider's route access rules.

Every app must complete backend authentication before becoming usable, including with no scopes. Protocol version 2 exposes only readiness and a one-use code in the iframe. Member IDs and profile fields require the app's authenticated REST calls; there is no browser-only identity flow. Zetapad enforces this boundary for its data, and the app host enforces its own route and session policy.

## Restrict app document requests

Apply a document-route guard on your server and send the [embedding CSP](https://zetapad.com/developers/docs/embedding) on every app HTML response:

```http
Content-Security-Policy: frame-ancestors https://zetapad.com https://www.zetapad.com;
Cache-Control: no-store
```

For strict browser launch control, accept the document only when `Sec-Fetch-Dest` is exactly `iframe`. A normal direct navigation sends `document` and receives `403`. Reject missing metadata in strict mode as well. `Sec-Fetch-Site` and `Referer` do not reliably identify the exact parent and must not be used as authentication.

```javascript
// Express-style middleware. Apply only to HTML document routes.
export function requireZetapadFrame(request, response, next) {
  // Strict mode: missing Fetch Metadata is rejected too.
  if (request.headers["sec-fetch-dest"] !== "iframe") {
    return response.status(403).set("Cache-Control", "no-store")
      .send("Open this app from Zetapad Rooms.");
  }
  next();
}

// Register every launch and app document route with the guard:
// app.get("/", requireZetapadFrame, serveBootstrap);
// app.get("/play", requireZetapadFrame, serveBootstrap);
// serveBootstrap serves only a public shell, with the frame-ancestors CSP.
// Assets and /zetapad/state and /zetapad/login use their own route handlers.
// Every protected API handler must verify its server-side Zetapad session.
```

Apply the guard to every app document route, including nested pages. Set the CSP in shared response middleware so it also reaches blocked and error pages. Asset requests, state/login endpoints, and app APIs are not document navigations and need their own handlers.

Fetch Metadata headers are browser launch controls. A non-browser client can spoof them, and unsupported browsers may omit them. Consequently, the returned shell must contain no private data or usable protected functionality. The backend session checks below are still required.

## Block top-level startup in the browser

Before the handshake or app initialization, check `window.self === window.top`. If true, show "Open this app from Zetapad Rooms" and stop. Leave protected UI hidden while loading; reveal it only after the backend login succeeds.

The [browser example](https://zetapad.com/developers/docs/examples/browser) rejects top-level startup before sending messages or making login requests. In an iframe, it verifies `event.source === window.parent` and the exact Zetapad parent origin, pins that origin after `ready`, and waits for verified backend login before `startYourApp`.

This check controls browser behavior. It cannot authenticate an API request or prevent someone from editing JavaScript on their own computer.

## Require a verified backend session

1. Generate a random, expiring state challenge in the visitor's server-side session. Do not reuse a shared state value.
2. Obtain a code through `authenticate` inside the trusted Zetapad iframe.
3. Validate and consume the echoed state in that same server-side session.
4. Exchange the code with your client ID and secret on the backend.
5. Fetch userinfo from Zetapad and establish app access for the verified `userId`.
6. On every protected API route, resolve the server session and check current Zetapad access before reading data or performing an action.

Use the [server example](https://zetapad.com/developers/docs/examples/server). `readZetapadMember(session)` rejects a missing or expired session token and checks userinfo for current access. Call it before each protected read or action. Your routing layer should return an authentication error if it fails and must not continue the protected handler.

Keep the token on the server. A query parameter such as `?from=zetapad`, a browser-supplied profile or `userId`, a `launchId` by itself, an `Origin` header, or a successful `ready` reply is not proof of backend authentication. A successful iframe `authenticate` reply provides a code to exchange; it does not establish your app's session. Even if your backend returns the ID for display, later requests must resolve identity from the server session rather than trust an ID sent back by the browser.

Use secure session cookies, CSRF protection for state-changing requests, and the exact session binding described in the examples. If a browser blocks your embedded session mechanism, show an error and stop initialization rather than granting anonymous access.

## What this guarantees

In supported browsers, document guards reject direct top-level navigation even when the visitor already has an app session. The CSP prevents other sites from embedding the app, and browser startup requires the trusted Zetapad parent. Protected APIs require a valid server-side Zetapad session and current token access.

The current protocol does not provide a server-verifiable signal proving that an iframe is still open for each subsequent API request. A valid app session can make API requests until your session policy, token expiry, or userinfo revocation check ends access. If you require per-request proof of a currently mounted iframe, that requires a separate protocol feature; do not claim an iframe check or CSP provides it.

## Acceptance checks

- Directly opening the launch URL returns `403`; a client-side fallback also prevents startup.
- Directly opening any nested app document returns the same blocked result, including with an existing app session.
- An unrelated site cannot embed the app because its origin is absent from `frame-ancestors`.
- A permitted iframe shell has no private data before authentication.
- The app initializes after a valid Zetapad handshake, consent, state validation, code exchange, and userinfo read.
- An unauthenticated call to every protected API route fails.
- Iframe authentication replies contain exactly `code`, `expiresIn`, and `state`.
- A spoofed profile, guessed launch ID, mismatched state, or reused code cannot establish app access.
- Expired tokens, member disconnection, unpublishing, deletion, and secret rotation prevent further protected reads and actions.
