---
title: "Sign-in on the agent's page"
description: "How a visitor signs into an agent's standalone page — Guuey sign-in, your identity endpoint, or a redirect to your own identity provider."
---

An agent's **standalone page** — its slug host, a
[custom domain](/domains/), or the first-party share page (its always-on
`<app-id>.agents.…` domain is the agent's API address and serves no page)
— has no host page to hand it a token, so it signs visitors in itself. Which way it does
that follows from the app's authentication mode (**Users → Authentication**
in the console, `--auth-mode` on the CLI):

| Authentication mode | On a custom domain                                                                                                                    | On a Guuey host (slug host / share page)                           |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| Anonymous           | guests                                                                                                                                | guests                                                             |
| Guuey sign-in       | **Guuey sign-in** — a full-page hop to Guuey's login, back to the page                                                                | same                                                               |
| Bring your own auth | your [identity endpoint](/identity-endpoint/) if you set one; else **your identity provider** if you registered a client; else guests | **your identity provider** if you registered a client; else guests |

The rest of this page is the last row: how bring-your-own apps sign
visitors in **without a Guuey account and without a Guuey sign-in moment**.

## Two mechanisms, one identity

Both bring-your-own paths end in the same place — a token the agent
verifies against **your** issuer, resolving to the same end-user identity
your [widget embed](/embed/#who-the-visitor-is) and [custom surface](/sdk/#signed-in-users-bring-your-own-auth)
resolve. One history, one memory, one profile, whichever surface the
person used.

- **Your identity endpoint** — silent. The page fetches a URL on your own
  site with the visitor's session cookie and gets a token back; no
  redirect, no login screen if they are already signed in with you. It
  only works where that cookie is reachable: **a custom domain** on the
  same site as your session. [Set it up →](/identity-endpoint/)
- **Your identity provider (OIDC redirect)** — a full-page hop to your
  IdP's login page and back, on **any** host the page is served on,
  including Guuey's own. Register one client at your IdP; Guuey runs the
  standard authorization-code flow (PKCE, `openid` scope) and hands the
  page the ID token your IdP issued. This page covers it.

If you configure both, the endpoint wins on your custom domain (it is
silent where it exists) and the redirect covers everywhere else.

How the token itself is verified — signature, issuer, audience, guests —
is the same on every surface; that model is on
[The identity model](/concepts-identity/).

## Set up the redirect

### 1. Register a client at your identity provider

Create an OIDC client (Auth0 "Regular Web App", a Cognito app client,
an Okta web app — any provider that publishes
`/.well-known/openid-configuration`) with **one** redirect URI, the
Guuey login relay's callback for your environment:

```
https://api.us-east-1.guuey.com/v1/auth/relay/complete
```

The console shows the exact value for your environment under
**Users → Authentication → Sign-in on standalone pages**, and
`guuey apps get` prints it once a client is registered. It is the same
URI for every app — Guuey tells the flows apart by a one-time state it
mints per sign-in.

Public client with PKCE is the default and works with most providers.
If yours insists on a confidential client, it may issue a client secret;
Guuey can hold it for you (sealed, never echoed) — see step 2.

### 2. Tell Guuey the client

The client's **Client ID must be the app's Audience**. That is not a
Guuey rule but an OpenID one: the token the page receives is the **ID
token**, and an ID token's `aud` claim is the client it was issued to.
The agent verifies every token against the app's Audience — the same
check that already applies to tokens your widget embed forwards, which is
why the [SDK page](/sdk/#signed-in-users-bring-your-own-auth) tells you
to forward the ID token, not the access token.

```bash
guuey apps update <appId> --auth-mode byo \
  --issuer-url https://login.example.com \
  --audience <client-id> \
  --oidc-client-id <client-id>
# confidential client:
#   … --oidc-client-secret <secret>      (sealed under KMS; pass an empty value to remove)
# turn page sign-in off again:
#   guuey apps update <appId> --issuer-url … --audience … --clear-oidc-client
```

Or in the console: **Users → Authentication → Bring Your Own Auth →
Sign-in on standalone pages** — the Client ID field shows the Audience as
its placeholder; type it (or your client id) to register the client.
Leaving the field empty keeps page sign-in off. (Client secrets are
stored through the CLI; the console shows whether one is stored.)

Guuey refuses a client on a Guuey-owned issuer — the app's own widget
issuer (`guuey widget keys create`) mints tokens server-to-server and has
no login page to send anyone to. This path is for **your** IdP.

### 3. That's it

Visitors on any of the app's standalone hosts see **Sign in**; the click
goes to your IdP's login and comes back on the page's root, signed in.
`guuey apps get` shows `Page sign-in: OIDC redirect (client …, public
client)` and the public agent card carries `oidcRelay: true`.

## What happens under the hood

1. **Start** — the page sends the visitor to
   `/v1/auth/relay/start?appId=…&returnTo=<its own URL>`. Guuey checks the
   `returnTo` against the hosts the app's page is served on (verified
   custom domains, the slug host, the share page — the app's own hosts,
   nothing else), fetches
   your issuer's discovery document, and redirects to its
   `authorization_endpoint` with `response_type=code`, `scope=openid`,
   PKCE (S256), a `state` and a `nonce`.
2. **Callback** — your IdP sends the browser back to the relay's callback
   with the code. Guuey checks the state (single-use, ten-minute
   lifetime, bound to the browser that started by an httpOnly cookie) and
   forwards the visitor to the page's root with a **≤60-second, single-use
   hand-back code** — never the IdP's code, never a token.
3. **Exchange** — the page redeems the hand-back code same-origin. Guuey
   POSTs your `token_endpoint` (client authentication as your discovery
   document allows), takes **only the ID token**, verifies it against your
   issuer's published keys, the app's Audience and the dance's `nonce`, and
   returns it to the page. The refresh token — if your IdP sent one — is
   discarded.
4. **Chat** — the page holds the ID token in memory and presents it as
   the bearer to the agent, exactly as an embed forwards your token. When
   it expires the page says _Session expired — sign in again_ and the hop
   repeats; with a live IdP session that is one round trip and no prompt.

Guuey stores nothing durable: no session, no refresh token, no account
for the visitor. The only rows are the two short-lived nonces above.

## Requirements and limits

- The issuer must publish `/.well-known/openid-configuration` whose
  `issuer` is your configured Issuer URL, with `https` endpoints on
  public hosts. Providers that advertise PKCE methods must include
  `S256`.
- The issuer's signing keys are read from `<issuer>/.well-known/jwks.json`
  (Auth0, Cognito, Clerk and most providers serve them there).
- Client ID = Audience (above). An app whose Audience is an API
  identifier rather than a client id cannot use page sign-in until it
  switches its embed to ID tokens.
- Client secrets are stored through the CLI only — the console shows
  whether one is stored. For a workspace-owned app, the CLI path is open
  to the app's creator and to workspace API keys.
- Visitors signed in this way have no Guuey/Portal account; their data
  lives under the identity your issuer asserts and is erased through the
  app's per-user erase (`guuey apps byo-user erase`).

## Troubleshooting

- **The page shows no _Sign in_ button.** The app is not `byo`, or no
  client is registered, or the Client ID does not equal the Audience, or
  the issuer is Guuey-owned. `guuey apps get` prints the current binding;
  `GET /v1/agents/<appId>` should carry `oidcRelay: true`.
- **`/start` answers `APP_NOT_RELAYABLE`.** Same causes as above — the
  server refuses what the page would not have offered.
- **`DISCOVERY_ISSUER_MISMATCH` / `PKCE_S256_UNSUPPORTED` / `*_BLOCKED`.**
  Your discovery document names a different `issuer` than the Issuer URL
  you configured (trailing slash aside), does not list `S256`, or names an
  endpoint on a non-https / private host.
- **The visitor lands back on `?guuey_auth_error=…`.** Your IdP declined
  (`access_denied`, a cancelled login) — the page offers to try again.
- **`ID_TOKEN_INVALID` at the exchange.** The ID token failed the same
  verification the agent runs: wrong audience (client ≠ Audience), wrong
  issuer, expired, or unsigned by the keys at
  `<issuer>/.well-known/jwks.json`.
- **`ID_TOKEN_MISSING`.** Your token endpoint answered without an
  `id_token` — an OAuth-only server, or `openid` was not granted.