Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 27 additions & 4 deletions docs/dashboard.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Optional web panel served by BeaconMCP on the same origin as the MCP endpoint (`https://<your-host>/app/...`). Three pages:

- **`/app/login`** — exchanges a client id + client secret + TOTP code for an MCP bearer, and stores it in a 90-day HttpOnly session cookie. Removes the need to issue `curl` requests from a phone.
- **`/app/login`** — exchanges a client id + client secret + a second factor (TOTP code or passkey) for an MCP bearer, and stores it in a 90-day HttpOnly session cookie. Removes the need to issue `curl` requests from a phone.
- **`/app/chat`** — multi-conversation chat with Gemini 2.5 Flash/Pro (stable) or Gemini 3 Flash / 3.1 Pro (preview, Google allowlist required). **Requires `GEMINI_API_KEY`.**
- **`/app/tokens`** — generates named bearers so external MCP clients (Gemini web, ChatGPT, Assistant Desktop) can be wired up without the OAuth dance. **Works without `GEMINI_API_KEY`.**

Expand Down Expand Up @@ -36,9 +36,32 @@ or `(chat: disabled, tokens only)` when the Gemini key is missing, or `disabled`
## Flow

1. Navigate to `https://<your-host>/` on any device — the root path redirects to `/app/login`.
2. Enter client id + client secret + current TOTP code (once per 90-day window).
3. Land on `/app/chat` (or `/app/tokens` in tokens-only mode) with the conversation history restored.
4. Every 24 hours the underlying MCP bearer expires. The dashboard prompts only for a fresh TOTP code on `/app/refresh`; the client id and secret remain encrypted server-side.
2. Enter client id + client secret, then the current TOTP code — or use a passkey instead, from the
link under the code boxes (see [Passkeys](#passkeys)). <kbd>Enter</kbd> validates as soon as the
sixth digit lands.
3. A confirmation screen shows when the MCP access and the signed-in session expire, offers to
register a passkey on this device, and waits for **Finish signing in**.
4. Land on `/app/chat` (or `/app/tokens` in tokens-only mode) with the conversation history restored.
5. Every 24 hours the underlying MCP bearer expires. The dashboard prompts only for a fresh TOTP code on `/app/refresh`; the client id and secret remain encrypted server-side.

With JavaScript disabled the page falls back to the previous behaviour: a plain form POST that
redirects straight to the landing page, no confirmation screen and no passkeys.

## Passkeys

A passkey replaces the 6-digit code, not the client secret — signing in still needs both factors.
Enrol one from the confirmation screen right after a normal sign-in, then on the next visit use
**Use a passkey instead** under the code boxes.

The same option is on the OAuth consent page (`/oauth/authorize`), which now also confirms before
redirecting: it shows when the granted access expires, offers passkey enrolment, and only mints the
authorization code when you press **Finish signing in**.

Registered passkeys are listed at the bottom of `/app/tokens` with their label, when they were added
and when they were last used, each with a **Remove** button.

Caveats — hostname binding, the HTTPS/loopback requirement, and why you should keep your TOTP seed —
are covered in [security.md](security.md#passkeys).

## Tokens page

Expand Down
45 changes: 45 additions & 0 deletions docs/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,51 @@ Unattended services (scheduled jobs, CI pipelines) sometimes genuinely need mach
case, with its required precautions, is covered separately in
[totp-automation.md](totp-automation.md). Read it end to end before deciding.

## Passkeys

A passkey (WebAuthn) replaces the **TOTP factor**, never the client secret. Both login pages keep the
same two-factor shape:

1. `client_id` + `client_secret`
2. a 6-digit code **or** a passkey assertion

That ordering is what makes a stolen passkey worthless on its own, and it is also a practical
constraint: the dashboard session encrypts the client secret so it can re-mint MCP bearers later, so
a fully usernameless login could not build a working session anyway.

Where they work:

| Page | Sign in with a passkey | Enrol a passkey |
|------|------------------------|-----------------|
| `/app/login` | Link under the 2FA step | On the post-2FA screen |
| `/oauth/authorize` | Link under the 2FA step | On the approval screen |

Credentials are stored in the dashboard database (`passkeys` table): a credential id, a **public**
key and a signature counter. Nothing secret leaves the authenticator.

Things worth knowing before you enrol:

- **Passkeys are bound to the hostname.** The relying-party ID is derived from the request host, so
a credential registered on `beacon.example` will not work on `beacon.internal` or on a raw IP that
differs from the one used at registration. Settle on your public hostname first.
- **A secure context is required.** Browsers only expose the WebAuthn API over HTTPS or on loopback.
On a plain-HTTP LAN deployment the passkey buttons are hidden and TOTP stays the only path in.
- **Keep TOTP working.** Passkeys are an alternative, not a replacement: losing every enrolled device
must not lock you out. The authenticator seed remains the recovery path.
- **Dynamically-registered clients delegate**, exactly like TOTP: a client created through the DCR
bootstrap is authorized by its *owner's* passkeys, so the second factor never leaves the owner.
- Registrations, revocations and passkey sign-ins are recorded in the audit log
(`dashboard.passkey.*`, `auth.passkey.*`, and `login`/`authorize` events tagged `via=passkey`).

Passkey ceremonies are rate-limited per IP by the same limiter that guards `/app/login`, and a TOTP
lockout also blocks the passkey path for that client.

Manage enrolled credentials from `/app/tokens`, or drop them all for a client with:

```sql
DELETE FROM passkeys WHERE client_id = 'beaconmcp_...';
```

## Audit trail

`server.audit_log` records tool calls, dashboard logins, OAuth authorizations and client revocations
Expand Down
3 changes: 3 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ dependencies = [
"python-dotenv",
"pyyaml",
"pyotp",
# Passkeys (WebAuthn) on the login / authorize pages. Imported lazily:
# the auth pages fall back to TOTP-only when it is missing.
"webauthn>=2,<4",
"qrcode",
"cryptography>=42",
"jinja2>=3",
Expand Down
Loading