> ## Documentation Index
> Fetch the complete documentation index at: https://docs.pylonsync.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Email + password

> Argon2id-hashed credentials with built-in registration, login, and timing-attack defenses.

For apps that need a password flow — shared devices, no email access, regulatory requirements — Pylon ships built-in email + password auth. Passwords are hashed with **Argon2id** (the OWASP-recommended algorithm); failed logins run a dummy hash so timing doesn't leak whether the email exists.

## Register

```bash theme={null}
curl -X POST https://your-app/api/auth/password/register \
  -H 'Content-Type: application/json' \
  -d '{
    "email": "alice@example.com",
    "password": "correct-horse-battery-staple",
    "displayName": "Alice"
  }'
```

Response (201):

```json theme={null}
{
  "token": "pylon_a1b2c3...",
  "user_id": "usr_xyz",
  "expires_at": 1735689600
}
```

The user is created and signed in — no separate "verify your email" step. If you need email verification, call `/api/auth/email/send-verification` after sign-in. The user can keep using the app while their email is unverified; gate sensitive flows on `User.emailVerified`.

### Validation rules

| Rule                          | Enforcement               |
| ----------------------------- | ------------------------- |
| Email contains `@`            | 400 `INVALID_EMAIL`       |
| Email is lowercased + trimmed | Server-side normalization |
| Password ≥ 8 characters       | 400 `WEAK_PASSWORD`       |
| Email not already registered  | 409 `EMAIL_TAKEN`         |

`displayName` defaults to the email if omitted.

## Log in

```bash theme={null}
curl -X POST https://your-app/api/auth/password/login \
  -H 'Content-Type: application/json' \
  -d '{"email": "alice@example.com", "password": "correct-horse-battery-staple"}'
```

Response (200):

```json theme={null}
{
  "token": "pylon_a1b2c3...",
  "user_id": "usr_xyz",
  "expires_at": 1735689600
}
```

Wrong credentials always return the same shape:

```json theme={null}
{ "error": { "code": "INVALID_CREDENTIALS", "message": "Email or password is incorrect" } }
```

The error message intentionally doesn't say which one is wrong — and the server runs the password hash check even when the email doesn't exist, so timing analysis can't enumerate accounts.

## From the SDKs

<CodeGroup>
  ```typescript TypeScript theme={null}
  import { configureClient } from "@pylonsync/react";

  configureClient({ baseUrl: "https://your-app" });

  // Register
  const reg = await fetch("/api/auth/password/register", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password, displayName: name }),
  }).then(r => r.json());

  // Login
  const login = await fetch("/api/auth/password/login", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ email, password }),
  }).then(r => r.json());
  ```

  ```swift Swift theme={null}
  import PylonClient

  let client = PylonClient(baseURL: URL(string: "https://your-app")!)
  let session = try await client.signInWithPassword(
      email: "alice@example.com",
      password: "correct-horse-battery-staple"
  )
  // token is persisted automatically
  ```
</CodeGroup>

## Why Argon2id?

Pylon uses [`argon2`](https://crates.io/crates/argon2) with the **Argon2id variant** — winner of the Password Hashing Competition and the OWASP recommendation. It's resistant to both side-channel attacks (Argon2i) and GPU-based brute-force (Argon2d) by design.

Default parameters:

| Parameter   | Value        | Why                                     |
| ----------- | ------------ | --------------------------------------- |
| Memory cost | 19 MiB       | OWASP minimum for interactive logins    |
| Time cost   | 2 iterations | Balances login latency vs attacker cost |
| Parallelism | 1            | Server-side default                     |
| Salt        | 16 bytes     | CSPRNG-generated per password           |

Each hash is self-describing — the algorithm parameters are stored in the hash string so future Pylon versions can rotate them without breaking existing passwords.

## Password reset

Pylon ships a built-in "forgot password" flow — two endpoints that email a
single-use reset link and swap the password once the user clicks it:

**1. Request a reset link.** The user submits their email:

```bash theme={null}
curl -X POST https://your-app/api/auth/password/reset/request \
  -H 'Content-Type: application/json' \
  -d '{"email": "alice@example.com"}'
```

Always returns `200 { "sent": true }` — whether or not the email is
registered. The endpoint is rate-limited and equalizes response timing
across the registered / not-registered paths, so it can't be used to
enumerate accounts. When the email exists, Pylon emails a link of the form
`<public-url>/reset-password?token=<token>`.

**2. Complete the reset.** Your `/reset-password` page reads the `token`
from the URL and POSTs it with the new password:

```bash theme={null}
curl -X POST https://your-app/api/auth/password/reset/complete \
  -H 'Content-Type: application/json' \
  -d '{"token": "<token>", "newPassword": "a-new-strong-password"}'
```

The token is single-use. The new password is length- and HIBP-checked (same
as register). On success Pylon updates `passwordHash`, **revokes every
existing session** for that user, and mints a fresh one:

```json theme={null}
{ "reset": true, "revoked_sessions": 3, "token": "pylon_...", "user_id": "usr_xyz", "expires_at": 1735689600 }
```

| Code             | Status | Meaning                                        |
| ---------------- | ------ | ---------------------------------------------- |
| `MISSING_EMAIL`  | 400    | No `email` on request                          |
| `MISSING_FIELD`  | 400    | `token` or `newPassword` missing on complete   |
| `WEAK_PASSWORD`  | 400    | New password shorter than 8 chars              |
| `PWNED_PASSWORD` | 400    | New password found in the HIBP breach corpus   |
| `INVALID_TOKEN`  | 401    | Reset token is wrong, expired, or already used |
| `RATE_LIMITED`   | 429    | Too many reset requests                        |

If you'd rather not surface a password-reset UI at all, you can instead run
users through the [magic-code](/auth/magic-codes) flow (which mints a session
without a password) and update `User.passwordHash` yourself from an
authenticated action.

## Configuring the User entity

Password auth expects a `User` entity with these fields (auto-created by `pylon init`):

```jsonc theme={null}
{
  "name": "User",
  "fields": [
    { "name": "email",        "type": "string",  "unique": true,  "optional": false },
    { "name": "displayName",  "type": "string",  "optional": false },
    { "name": "passwordHash", "type": "string",  "optional": true },
    { "name": "avatarColor",  "type": "string",  "optional": true },
    { "name": "emailVerified","type": "datetime","optional": true },
    { "name": "createdAt",    "type": "datetime","optional": false }
  ]
}
```

`passwordHash` is optional because users who signed up via OAuth or magic code never had one. `emailVerified` is null until they prove control of the email.

## Security notes

* **Never deserialize `AuthContext` from request body.** The Rust side intentionally doesn't derive `Deserialize` so a client can't forge `is_admin: true`. Identity comes from the session lookup, not the wire.
* **`/api/auth/session` POST is gated** — only dev mode or admin token can mint a session for an arbitrary user\_id. Your registration/login endpoints are the only public ways to obtain a token.
* **Sessions expire after 30 days by default** — see [Sessions](/auth/sessions) to change.
* **`/login` is rate-limited by default** — the built-in per-IP/per-email limiter throttles the credential bucket (login, register, reset, TOTP verify) and returns `429 RATE_LIMITED` with a `retry_after` hint on abuse. Pylon Cloud layers additional per-IP limiting at the edge.

## When to use password vs alternatives

Use password when:

* Email isn't reliable (offline-first apps, regions with poor SMTP delivery)
* Compliance requires it
* Users explicitly prefer it

Otherwise prefer **magic codes** (no memory load, email is verified by construction) or **OAuth** (zero credentials your service stores).
