---
title: Auth-Gated UI
description: Gate ggui renders behind your app's auth with a bearer token on useMcpAppsChat — and understand why OAuth tool-consent is the client's job, not ggui's.
---

ggui is a renderer, not an identity provider. Your app already knows who the user is. There are **two separate auth concerns**, and only the first is yours to wire:

1. **Authenticating your app → agent backend.** Every request `useMcpAppsChat` makes (prompt POST, snapshot GET, the iframe → MCP tool-call relay) carries a bearer token your app supplies. The backend gates chat ownership on the principal that token identifies. **This is the recipe below.**
2. **OAuth consent for tools the _agent_ calls.** When a tool the agent calls needs the end-user to authorize a third-party service (Google, Slack), that consent flow is **not** ggui's responsibility — it belongs to MCP's OAuth standard, the hosting agent, and your client app. ggui only surfaces an optional signal. See [OAuth tool-consent is the client's job](#oauth-tool-consent-is-the-clients-job).

## Gate the chat behind a bearer token

`useMcpAppsChat` takes two auth hooks:

- **`getAuthToken()`** — called before every request; return the bearer token to send as `Authorization: Bearer <token>` (or `undefined` for none).
- **`onUnauthenticated()`** — called on a `401`; refresh/re-mint your token, return `true` to retry the request once, `false` to surface the error.

The [`ggui-basic-web`](https://github.com/ggui-ai/ggui/tree/main/samples/apps/ggui-basic-web) sample uses these for a **guest-token** flow: it mints an anonymous token from the backend's `POST /auth/guest` mount, caches it, and re-mints on `401`. Swap that mint for your own user-session token and the same wiring gates renders behind a signed-in user:

```tsx
import { useCallback, useRef } from "react";
import { useMcpAppsChat } from "@ggui-ai/react/chat-helpers";

function Chat({ agentEndpoint }: { agentEndpoint: string }) {
  const { user, getAccessToken, refreshSession } = useAuth(); // your auth provider

  // Read the current token per-request (kept in a ref so the callback
  // always sees the latest after a refresh).
  const tokenRef = useRef<string | undefined>(getAccessToken());
  const getAuthToken = useCallback(() => tokenRef.current, []);

  // 401 → refresh once, then retry. Return false to give up.
  const onUnauthenticated = useCallback(async () => {
    const fresh = await refreshSession();
    if (!fresh) return false;
    tokenRef.current = fresh;
    return true;
  }, [refreshSession]);

  const { entries, sessions, send, handleAppMessage } = useMcpAppsChat({
    chatEndpoint: `${agentEndpoint}/agent`,
    snapshotEndpoint: `${agentEndpoint}/agent`,
    getAuthToken,
    onUnauthenticated,
  });

  // render `entries` + mount `sessions` with <AppRenderer> as usual
}
```

The token rides every hook request automatically. The backend authenticates it and scopes the conversation (its `chatId` snapshot + resume state) to that principal — clear the session and you're a different principal with different chats.

### Mount-time gate

Don't open the chat until your provider says the user is signed in — a plain conditional render, exactly how the sample guards on `guestTokenReady`:

```tsx
function App({ agentEndpoint }: { agentEndpoint: string }) {
  const { isAuthenticated, isLoading } = useAuth();
  if (isLoading) return <p>Loading…</p>;
  if (!isAuthenticated) return <LoginPage />;
  return <Chat agentEndpoint={agentEndpoint} />;
}
```

### The tool-call relay carries the token too

When the iframe dispatches a tool call, your host relays it to the agent backend's `POST /agent` (`kind: "tool-call"`) inside `<AppRenderer onCallTool>`. Send the **same** bearer token on that relay so the MCP call runs as the signed-in user — the sample's `onCallTool` reads `getAuthToken()` and sets `Authorization: Bearer <token>` on the relay fetch. Don't trust anything the iframe puts in the payload as identity; the token on the relay is the credential.

## OAuth tool-consent is the client's job

A different situation: the agent wants to call a tool that needs the **end-user** to authorize a third-party service first (read their Google Calendar, post to Slack). That consent flow is deliberately **outside** the ggui protocol. It is owned by:

- **MCP's OAuth standard** — the spec defines how an MCP server advertises that a tool needs authorization and how the consent/token exchange happens.
- **The hosting agent** (e.g. `@anthropic-ai/claude-agent-sdk`) — follows that spec to handle tool authorization.
- **Your end-user client app** — the surface actually talking to the agent backend pops its own consent UI and drives the redirect. Consent is a client decision, not something ggui's runtime or protocol renders.
- **Guuey's MCP proxy** — captures an OAuth credential once so multiple agents can reuse it (coming soon — part of the hosted Guuey platform, not the open ggui protocol).

So ggui itself renders no consent screen. What it _does_ provide is an **optional helper signal**: when the server emits a `system` frame with `action: "auth_required"`, the iframe-runtime projects it to a typed `AuthRequiredEvent` on the `ggui:observe` postMessage channel (`{ type: "ggui:observe", event }`). A host that wants to surface a consent prompt can listen for it and open `authUrl` in a popup — but rendering that prompt, and the OAuth exchange itself, are your client's job:

```ts
import type { AuthRequiredEvent } from "@ggui-ai/react";

// AuthRequiredEvent shape (kind: "auth-required"):
//   provider     — canonical service id ("google", "slack")
//   authUrl      — URL to open to start the OAuth consent flow
//   displayName? — human-readable service name
//   scopes?      — requested OAuth scopes
//   message?     — why access is needed
```

It's a projection of the protocol's `SystemPayload`, not a new ggui surface — surfacing it is opt-in, and ggui draws nothing itself. See [Error Handling → renderer-side faults](/cookbook/error-handling/#renderer-side-faults-stay-inside-the-iframe) for the full observability catalog.

## See also

- [React SDK](/sdk/react/) — `useMcpAppsChat` (`getAuthToken` / `onUnauthenticated`) + `<AppRenderer>`
- [`ggui-basic-web` sample](https://github.com/ggui-ai/ggui/tree/main/samples/apps/ggui-basic-web) — the runnable guest-token reference (`Chat.tsx`)
- [Error Handling](/cookbook/error-handling/) — HTTP `401`, JSON-RPC auth codes, and the `auth-required` observability event
- [Getting Started](/getting-started/) — the agent-side handshake → render → consume loop