---
type: explanation
---

# Account login

How customers sign in to the [dashboard](dashboard.md). The other three [management modalities](management-api.md) (OpenAPI, MCP, Terraform) authenticate with a `cpt_pat_*` Personal Access Token — see [management-api](management-api.md#authentication). This page is the dashboard side.

## Three sign-in paths, one account

| Path | What you give the platform | What we send back |
|---|---|---|
| Magic link | An email address | A single-use URL valid for 15 minutes |
| Continue with GitHub | A GitHub OAuth consent | A session cookie (after we read your primary verified email) |
| Continue with Google | A Google OAuth consent | A session cookie (after we read your verified email from the id_token) |

All three resolve to the same account row keyed by your email. A customer who signs in with GitHub today and a magic-link tomorrow lands on the same account; a customer who signs in with Google to an address that already has a GitHub-bound caputchin account gets attached to the same row. The first sign-in for a new email creates the account; subsequent sign-ins find it.

## What we store

One column of personal data per account: **your email**.

We do not store your name, your provider profile photo, your avatar, your locale, your given name, your family name, your provider display name, your timezone, or any other field the OAuth provider hands us. The provider response is read once at callback, used to assert a verified email, and discarded before any log line. See [privacy](privacy.md#what-we-collect-about-you-the-customer).

The OAuth provider binding itself (which provider account proved control of which email) is stored in a separate table as `(provider, provider_sub, account_id)` — `provider_sub` is the provider's stable user id. It's not personally identifying on its own and exists so we can detect "same GitHub account, different email later" attempts.

## Magic link

You submit an email address. We HMAC-sign a token containing your email + a single-use nonce + a 15-minute expiry, and email you the URL. Clicking it atomically claims the nonce — if you click twice, only the first click signs you in. If you don't click within 15 minutes, the link is dead and you request another.

Rate limit: one in-flight magic-link per email address per 60 seconds. If you ask for a second link within that window we silently rate-limit so we don't leak whether the address exists in our account base; you still see the same "check your email" page either way.

## OAuth — GitHub and Google

Both use the standard authorization-code flow. Google additionally uses PKCE; GitHub's arctic client doesn't expose it, so we rely on the signed state cookie alone — equivalent guarantee since the state is HMAC-signed and bound to the provider in the same envelope.

For both providers we require **verified email**:

- **GitHub** — we fetch `/user/emails` and accept only the address where `primary && verified`. If your primary GitHub email isn't verified, you can't use this path; verify it on GitHub or use a magic link to the same address.
- **Google** — we read `email_verified === true` from the id_token. Unverified addresses are rejected.

If your provider rejects the sign-in mid-flow (you click Cancel, your org's IdP refuses consent, etc.), you land back on `/login` with a clear error.

## Sessions

When sign-in succeeds, the platform sets a `caput_session` cookie carrying an opaque session id. The cookie is `HttpOnly`, `SameSite=Lax`, and `Secure` in production. The actual session is a row in `user_sessions` — the cookie is just a pointer.

| Property | Value |
|---|---|
| TTL | 30 days from last use (rolling) |
| Authority | The database row, not the cookie |
| Refresh cadence | `last_seen_at` ticks at most once per minute; full expiry rolled once per 5 days |
| Invalidation | Sign out, or the row is deleted |

Sign-out is a POST to `/api/auth/sign-out` from anywhere in the dashboard — the button lives in the dashboard sidebar. The server deletes the row and clears the cookie.

There is no "remember me" toggle and no "sign in across all browsers" flow at MVP. Every successful sign-in creates a new session row scoped to that browser; signing in on a second device gives you a second row.

## What this is not

- **Not a single-sign-on for your customers.** Caputchin's own users (you, the integrator) sign in via this flow. The end-users of the CAPTCHA never authenticate to us in any sense — see [principles](principles.md#privacy-is-structural).
- **Not a password reset flow.** There are no passwords. If you lose access to your email, you lose access to the account; recovery happens out-of-band (support).
- **Not a team / multi-user model.** Each account is single-user at MVP. Multi-user organisations are deferred to the Enterprise tier — see [roadmap](roadmap.md).

## See also

- [dashboard](dashboard.md) — what the signed-in surface exposes
- [management-api](management-api.md) — how the four modalities (including this one) relate
- [privacy](privacy.md) — what we don't collect about your end-users
- [guides/sign-up-and-login](guides/sign-up-and-login.md) — step-by-step instructions
