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

# Authentication

# Authenticate with the Nora API

> Sign up, log in, and manage your Nora account via the Auth API. Includes JWT token usage, profile updates, and password management.

Every protected Nora API endpoint requires an authenticated identity — either a **session JWT** (covered on this page) or a **workspace API key** with scopes (see the [API overview](/api/overview#authentication) for the key-and-scope model). You obtain a JWT by signing up and then logging in; it is valid for **7 days**, sent as a Bearer token in the `Authorization` header of every subsequent request, and also accepted as an HttpOnly session cookie. This page covers all authentication endpoints and shows you exactly what each one returns.

<Warning>
  An explicit `Authorization`, `X-Api-Key`, or `X-Nora-Api-Key` credential takes precedence over the
  ambient `nora_auth` cookie. Do not send conflicting explicit credentials: Nora rejects them with
  `400 conflicting_auth` before either token is verified.
</Warning>

<Note>
  Login and OAuth auth endpoints are rate-limited to **20 requests per 15-minute window** per IP.
  Public signup has stricter burst and daily rate limits, plus Turnstile or reCAPTCHA verification
  when configured. Hosted PaaS mode requires one of those providers and fails closed when neither is
  configured; self-hosted mode may explicitly use `none`.
</Note>

## Bootstrap status

Check whether a self-hosted server still needs its first admin. This drives the "claim this server"
first-run flow: until the first user registers (that first account becomes the platform admin),
`needsFirstAdmin` is `true`; afterward it is `false`. Hosted PaaS requires an explicit bootstrap
administrator before startup and never reports public first-admin claim. The response also contains runtime OAuth visibility, normalized platform
mode, and signup-challenge metadata so published frontend images can render the
correct auth surface without build-time replacement. Verification secrets are never
returned. A user count or emails would aid account enumeration. See the [security
overview](/concepts/security) for the broader claim flow.

```
GET /auth/bootstrap-status
```

Public — no authentication required.

### Response

<ResponseField name="needsFirstAdmin" type="boolean">
  `true` only while an empty self-hosted installation permits first-account admin claim. Hosted PaaS
  always reports `false`.
</ResponseField>

<ResponseField name="oauthLoginEnabled" type="boolean">
  Whether the running backend accepts OAuth login. Login and signup hide OAuth controls unless this
  value is `true`.
</ResponseField>

<ResponseField name="platformMode" type="string">
  Normalized deployment mode: `selfhosted` or `paas`.
</ResponseField>

<ResponseField name="signupBotProtection" type="object">
  Safe public runtime configuration: `enabled`, `provider`, public `siteKey`, `configured`, and a
  user-safe `configurationError`. Secret verification keys remain backend-only.
</ResponseField>

```bash theme={null}
curl http://localhost:8080/api/auth/bootstrap-status
```

```json theme={null} theme={null}
{
  "needsFirstAdmin": true,
  "oauthLoginEnabled": false,
  "platformMode": "selfhosted",
  "signupBotProtection": {
    "enabled": true,
    "provider": "turnstile",
    "siteKey": "0x4AAAA...",
    "configured": true,
    "configurationError": null
  }
}
```

***

## Sign up

Create a new user account. On an empty self-hosted installation, the **first** registered user
becomes the platform admin (the first-admin claim flow — see [bootstrap status](#bootstrap-status)
and the [security overview](/concepts/security)). Hosted PaaS must seed its administrator before
startup, so public signup there always creates a regular user.

```
POST /auth/signup
```

### Request body

<ParamField body="email" type="string" required>
  A valid email address, maximum 255 characters.
</ParamField>

<ParamField body="password" type="string" required>
  Password, minimum 8 characters and maximum 128 characters.
</ParamField>

<ParamField body="botProtectionToken" type="string">
  Optional challenge token. Required only when `SIGNUP_BOT_PROTECTION_PROVIDER` is set to
  `turnstile` or `recaptcha`.
</ParamField>

### Response

<ResponseField name="id" type="string">
  The new user's UUID.
</ResponseField>

<ResponseField name="email" type="string">
  The registered email address.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null} theme={null}
  curl -X POST http://localhost:8080/api/auth/signup \
    -H "Content-Type: application/json" \
    -d '{"email":"you@example.com","password":"securepassword"}'
  ```
</CodeGroup>

```json theme={null} theme={null}
{
  "id": "d4e5f6a7-b8c9-4d01-a234-56789bcdef01",
  "email": "you@example.com"
}
```

### Error responses

| Status | Condition                                     |
| ------ | --------------------------------------------- |
| `400`  | Missing or invalid email/password             |
| `403`  | Bot protection token is missing or invalid    |
| `409`  | Account already exists for this email         |
| `429`  | Signup burst or daily rate limit was exceeded |

***

## Log in

Exchange email and password for a JWT.

```
POST /auth/login
```

### Request body

<ParamField body="email" type="string" required>
  Your registered email address.
</ParamField>

<ParamField body="password" type="string" required>
  Your password.
</ParamField>

### Response

<ResponseField name="token" type="string">
  A signed JWT valid for 7 days. Include this value in the `Authorization: Bearer <token>` header on all subsequent requests.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null} theme={null}
  curl -X POST http://localhost:8080/api/auth/login \
    -H "Content-Type: application/json" \
    -d '{"email":"you@example.com","password":"securepassword"}'
  ```
</CodeGroup>

```json theme={null} theme={null}
{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
```

### Error responses

| Status | Condition                                        |
| ------ | ------------------------------------------------ |
| `400`  | Email or password missing                        |
| `401`  | Invalid credentials, or account uses OAuth login |
| `429`  | Rate limit exceeded                              |

***

## Using the token

Pass the token you received from `/auth/login` in every request to a protected endpoint:

```
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```

<Warning>
  Tokens expire after 7 days. Your client should catch `401` responses and redirect the user to log
  in again.
</Warning>

***

## Get current user

Verify a token and retrieve the authenticated user's profile.

```
GET /auth/me
```

**Requires authentication.**

### Response

<ResponseField name="id" type="string">
  User UUID.
</ResponseField>

<ResponseField name="email" type="string">
  Email address.
</ResponseField>

<ResponseField name="name" type="string">
  Display name, may be `null` if not set.
</ResponseField>

<ResponseField name="role" type="string">
  Account role. Typically `user` or `admin`.
</ResponseField>

<ResponseField name="provider" type="string">
  OAuth provider (`github`, `google`, etc.) or `null` for password accounts.
</ResponseField>

<ResponseField name="avatar" type="string">
  Base64-encoded `data:image/...` avatar, or `null`.
</ResponseField>

<ResponseField name="created_at" type="string">
  ISO 8601 timestamp of account creation.
</ResponseField>

```bash theme={null}
TOKEN="paste-your-token-here"
curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/auth/me
```

```json theme={null} theme={null}
{
  "id": "d4e5f6a7-b8c9-4d01-a234-56789bcdef01",
  "email": "you@example.com",
  "name": "Alex Smith",
  "role": "user",
  "provider": null,
  "avatar": null,
  "created_at": "2025-01-15T10:30:00.000Z"
}
```

***

## Update profile

Update your display name and/or avatar.

```
PATCH /auth/profile
```

**Requires authentication.**

### Request body

<ParamField body="name" type="string">
  Display name, 1–100 characters.
</ParamField>

<ParamField body="avatar" type="string">
  Base64-encoded image (`data:image/png;base64,...`), maximum 500 KB. Pass `null` to remove the
  avatar.
</ParamField>

### Response

<ResponseField name="name" type="string">
  Updated display name.
</ResponseField>

<ResponseField name="avatar" type="string">
  Updated avatar value or `null`.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null} theme={null}
  curl -X PATCH http://localhost:8080/api/auth/profile \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"name":"Alex Smith"}'
  ```
</CodeGroup>

```json theme={null} theme={null}
{
  "name": "Alex Smith",
  "avatar": null
}
```

***

## Change password

Replace your current password with a new one.

```
PATCH /auth/password
```

**Requires authentication.** Not available for OAuth-only accounts.

### Request body

<ParamField body="currentPassword" type="string" required>
  Your existing password.
</ParamField>

<ParamField body="newPassword" type="string" required>
  New password, minimum 8 characters and maximum 128 characters.
</ParamField>

### Response

<ResponseField name="success" type="boolean">
  `true` when the password was updated successfully.
</ResponseField>

<CodeGroup>
  ```bash curl theme={null} theme={null}
  curl -X PATCH http://localhost:8080/api/auth/password \
    -H "Authorization: Bearer $TOKEN" \
    -H "Content-Type: application/json" \
    -d '{"currentPassword":"oldpass","newPassword":"newpass123"}'
  ```
</CodeGroup>

```json theme={null} theme={null}
{ "success": true }
```

### Error responses

| Status | Condition                                                  |
| ------ | ---------------------------------------------------------- |
| `400`  | Missing fields, or account has no password (OAuth account) |
| `401`  | Current password is incorrect                              |
| `404`  | User record not found                                      |

***

## OAuth login

Exchange a server-verified OAuth identity for a Nora session. On success this
returns a `token` plus the `user` record and sets the HttpOnly session cookie.

```
POST /auth/oauth-login
```

<Note>
  This endpoint is **disabled by default**. Unless `OAUTH_LOGIN_ENABLED=true`, it returns `403`.
  Google or GitHub credentials must also be configured in the marketing OAuth bridge. Provider
  tokens are verified server-side before Nora issues a session. Like password login, this endpoint
  is rate-limited to **20 requests per 15-minute window** per IP.
</Note>

### Request body

<ParamField body="provider" type="string" required>
  OAuth provider identifier (for example `github` or `google`).
</ParamField>

<ParamField body="oauthAccessToken" type="string">
  Provider access token. Either this or `oauthIdToken` is required.
</ParamField>

<ParamField body="oauthIdToken" type="string">
  Provider ID token. Either this or `oauthAccessToken` is required.
</ParamField>

### Response

<ResponseField name="token" type="string">
  A signed JWT for the linked or newly created account.
</ResponseField>

<ResponseField name="user" type="object">
  The authenticated user record.
</ResponseField>

***

## Upgrade session

Mirror an existing Bearer-token session into the browser's HttpOnly session
cookie. The supplied token is re-verified, so a forged Bearer is rejected.

```
POST /auth/session-upgrade
```

**Requires authentication.** Send the token in the `Authorization: Bearer <token>` header.

### Response

<ResponseField name="success" type="boolean">
  `true` when the cookie was set.
</ResponseField>

```json theme={null} theme={null}
{ "success": true }
```

***

## Log out

Clear the session cookie.

```
POST /auth/logout
```

This endpoint intentionally requires **no authentication**, so a page holding a
stale or invalid cookie can still clean itself up.

### Response

<ResponseField name="success" type="boolean">
  `true` when the cookie was cleared.
</ResponseField>

```json theme={null} theme={null}
{ "success": true }
```
