Canonical page: https://docs.singlebase.io/auth

# Authentication

Register users, sign them in, and manage their sessions with `auth.*`. Read and update the signed-in user's profile with `user.*`.

## Overview

**Identity** gives your users a way in. Every project includes a complete user system: sign-up, sign-in, sessions, and access control, with no backend code. Your agents work with your data; your users get their own accounts and see their own records.

**Key features:**
- Email and password signup and signin
- Passwordless signin with one-time email codes
- OAuth with Google, GitHub, Facebook, and LinkedIn
- Short-lived ID tokens with rotating refresh tokens
- Password reset, email change, and username change with codes
- Self-service profile updates

**How it works:**
1. The user signs in with `auth.signin` and receives an `id_token` and a `refresh_token`.
2. Your app sends `Authorization: Bearer <id_token>` on requests that act as the user.
3. Before the ID token expires, call `auth.refresh` to get a new pair.

To create or manage users from your server or an agent, use [Users](/users.md). To add sign-in to a website without writing UI, use the [Auth UI](https://singlebase.io/components/auth-ui/) web component.

---

## Operations

| Operation | Description | Bearer token |
|---|---|---|
| `auth.settings` | Read public authentication settings | No |
| `auth.signup` | Create an email/password account | No |
| `auth.signin` | Sign in with a password or an email code | No |
| `auth.refresh` | Rotate session tokens | No (tokens go in the payload) |
| `auth.signout` | Revoke all of the user's sessions | Required |
| `auth.request_code` | Email a one-time code for a purpose | No |
| `auth.confirm_code` | Reset a password or change email/username with a code | No |
| `auth.change_password` | Set a new password for the current user | Required |
| `auth.nonce` | Get a nonce for OAuth | No |
| `auth.oauth_connect` | Start OAuth signin, signup, or account linking | No |
| `auth.oauth_signin` | Exchange an OAuth access code for a session | No |
| `user.get` | Get the current user's profile | Required |
| `user.update` | Update the current user's profile | Required |

**Audience:** Authentication flows that use account identity accept `aud`, which defaults to `"web"`. `auth.settings` and `auth.nonce` need no payload fields. Use the same audience for the whole flow. Self-service signup always uses the default audience and roles.

---

## Sign Up

### auth.signup

Create an email/password account.

```json
{
  "operation": "auth.signup",
  "payload": {
    "email": "jane@example.com",
    "password": "ExamplePass123!",
    "first_name": "Jane",
    "last_name": "Doe",
    "metadata": { "plan": "free" }
  }
}
```

**Parameters:**

| Field | Required | Description |
|---|---|---|
| `email` | Yes | Email address. Stored in lowercase |
| `password` | Yes | Must meet the project's password policy |
| `first_name` | Yes | First name. Alias `name` |
| `last_name` | No | Last name. Alias `surname` |
| `phone` | No | Phone number. Alias `phone_number` |
| `profile_photo` | No | Photo URL. Alias `photo_url` |
| `metadata` | No | Custom profile data |

**Response:**
```json
{
  "data": {
    "id": "8f14e45fceea167a5a36dedd4bea2543",
    "next_action": "SIGNIN",
    "next_operation": "auth.signin"
  }
}
```

**Behavior:**
- Signup does not sign the user in. Call `auth.signin` next.
- `next_action` is `SIGNIN_WITH_CODE` when the project requires email verification or an email second factor.
- The response is the same whether or not the email already exists, so it never reveals registered accounts.
- Email subaddresses such as `jane+app@example.com` are rejected.
- Sending `roles` or a custom `aud` returns `CUSTOM_SIGNUP_CLAIMS_NOT_ALLOWED`.

---

## Sign In

### auth.signin

**Example - Password:**
```json
{
  "operation": "auth.signin",
  "payload": {
    "email": "jane@example.com",
    "password": "ExamplePass123!"
  }
}
```

**Example - Email code** (after [`auth.request_code`](#auth-request-code) with `purpose: "signin"`):
```json
{
  "operation": "auth.signin",
  "payload": {
    "email": "jane@example.com",
    "grant_type": "code",
    "code": "123456"
  }
}
```

**Parameters:**

| Field | Description |
|---|---|
| `email` | Email address (required) |
| `grant_type` | `"password"` (default) or `"code"` |
| `password` | Password, for password signin |
| `code` | One-time code, for code signin. Alias `otp` |
| `purpose` | `"invite"` when accepting an invitation |

When the project requires email verification or an email second factor, send both `password` and `code`.

**Response:**
```json
{
  "data": {
    "id_token": "<ID_TOKEN>",
    "refresh_token": "<REFRESH_TOKEN>",
    "token_type": "bearer",
    "user_profile": {
      "id": "user_123",
      "email": "jane@example.com",
      "first_name": "Jane",
      "last_name": "Doe",
      "aud": "web",
      "roles": ["user"]
    },
    "next_action": null,
    "next_operation": null,
    "token_info": {
      "ttl": 900,
      "exp": 1791044100,
      "iat": 1791043200,
      "aud": "web",
      "id": "user_123"
    }
  }
}
```

The profile is abbreviated. See [`user.get`](#user-get) for every field.

**Tokens:**
- **id_token** - Send as `Authorization: Bearer <id_token>`. Lasts 15 minutes
- **refresh_token** - Store securely. Exchange with `auth.refresh`. Sessions last 30 days

**Example - Accept an invitation:**

Request a code with `purpose: "invite"`, then set the password while signing in:

```json
{
  "operation": "auth.signin",
  "payload": {
    "email": "jane@example.com",
    "grant_type": "code",
    "purpose": "invite",
    "code": "123456",
    "password": "ExamplePass123!"
  }
}
```

---

## Sessions

### auth.refresh

Get a new token pair. The ID token may already be expired, but its session must still be valid.

```json
{
  "operation": "auth.refresh",
  "payload": {
    "id_token": "<ID_TOKEN>",
    "refresh_token": "<REFRESH_TOKEN>"
  }
}
```

Returns a new session, in the same shape as `auth.signin`. Replace both stored tokens: the old refresh token is consumed, and reusing it fails.

### auth.signout

Revoke every session for the current user. Send the ID token in the `Authorization` header.

```json
{
  "operation": "auth.signout",
  "payload": {}
}
```

Returns `data: {"signout": true}`. Clear the tokens stored in your app.

**Example - Keep a session alive:**
```javascript
async function refreshSession(session) {
  const result = await singlebase('auth.refresh', {
    id_token: session.id_token,
    refresh_token: session.refresh_token
  });
  return result.data; // store the new id_token and refresh_token
}
```

---

## One-Time Codes

### auth.request_code

Email a one-time code for a specific purpose.

```json
{
  "operation": "auth.request_code",
  "payload": {
    "email": "jane@example.com",
    "purpose": "password_reset"
  }
}
```

**Response:**
```json
{
  "data": {
    "accepted": true,
    "purpose": "password_reset",
    "next_action": "RESET_PASSWORD_WITH_CODE",
    "next_operation": "auth.confirm_code"
  }
}
```

| Purpose | `next_action` | Next operation | Field to send with the code |
|---|---|---|---|
| `signin` | `SIGNIN_WITH_CODE` | `auth.signin` | None |
| `invite` | `ACCEPT_INVITE_WITH_CODE` | `auth.signin` | `password` |
| `password_reset` | `RESET_PASSWORD_WITH_CODE` | `auth.confirm_code` | `new_password` |
| `email_change` | `CHANGE_EMAIL_WITH_CODE` | `auth.confirm_code` | `new_email` |
| `username_change` | `CHANGE_USERNAME_WITH_CODE` | `auth.confirm_code` | `new_username` |

**Behavior:**
- Codes expire after 10 minutes and work once.
- Codes are bound to the account, audience, and purpose.
- `accepted: true` does not reveal whether the account exists or a code was sent.
- Password recovery and account changes require the matching email-code setting in the project.

### auth.confirm_code

Use a code to reset a password or change an email or username.

```json
{
  "operation": "auth.confirm_code",
  "payload": {
    "email": "jane@example.com",
    "purpose": "password_reset",
    "code": "123456",
    "new_password": "NewExamplePass123!"
  }
}
```

**Parameters:**

| Field | Description |
|---|---|
| `email` | Current account email (required) |
| `purpose` | `password_reset`, `email_change`, or `username_change` (required) |
| `code` | One-time code (required). Alias `otp` |
| `new_password`, `new_email`, `new_username` | The value for the purpose (required) |

For an email change, the code goes to the current email. The new email starts unverified.

Confirmation revokes existing sessions and returns a new session. Store the new tokens.

---

## Passwords

### auth.change_password

Set a new password for the signed-in user. Send the ID token in the `Authorization` header.

```json
{
  "operation": "auth.change_password",
  "payload": {
    "password": "NewExamplePass123!"
  }
}
```

Revokes existing sessions and returns a new session. Send plain passwords only; `password_hash` and `password_algo` are rejected.

**Forgot password flow:**
1. `auth.request_code` with `purpose: "password_reset"`
2. The user enters the emailed code
3. `auth.confirm_code` with `code` and `new_password`
4. Store the session it returns

---

## OAuth

Sign users in with Google, GitHub, Facebook, or LinkedIn. Providers must be enabled in the project.

**Flow:**
1. Call `auth.nonce` and keep the nonce in the browser.
2. Call `auth.oauth_connect` with the provider, nonce, and intent.
3. Redirect the browser to `oauth_redirect_url`.
4. The provider returns to Singlebase, which redirects to your project's OAuth return URL with an `access_code`.
5. Call `auth.oauth_signin` with the `access_code` and the same nonce.

### auth.nonce

```json
{
  "operation": "auth.nonce",
  "payload": {}
}
```

Returns `data: {"nonce": "<NONCE>"}`. The nonce expires after 10 minutes.

### auth.oauth_connect

```json
{
  "operation": "auth.oauth_connect",
  "payload": {
    "provider": "google",
    "nonce": "<NONCE>",
    "intent": "signin"
  }
}
```

**Parameters:**

| Field | Description |
|---|---|
| `provider` | `google`, `github`, `facebook`, or `linkedin` (required) |
| `nonce` | Nonce from `auth.nonce` (required) |
| `intent` | `signin` (default), `signup`, or `link` |
| `id_token`, `refresh_token` | Current session. Required for `link` |

OAuth signup requires a verified email. Names are optional; if the provider omits a name, the full email address is used as `first_name`.

**Response:**
```json
{
  "data": {
    "oauth_redirect_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
    "oauth_provider": "google"
  }
}
```

**Return URL parameters:**

| Parameter | Meaning |
|---|---|
| `access_code` | Success. Exchange it with `auth.oauth_signin` |
| `error=oauth_denied` | The user cancelled at the provider |
| `oauth_error=<CODE>` | The flow failed. See the codes below |

| `oauth_error` | Meaning |
|---|---|
| `OAUTH_VERIFICATION_FAILED` | The provider response could not be verified |
| `VERIFIED_PROVIDER_EMAIL_REQUIRED` | The provider did not return a verified email |
| `SIGN_IN_TO_LINK_PROVIDER` | The email belongs to an account; sign in, then link |
| `PROVIDER_ALREADY_LINKED` | The provider identity belongs to another account |
| `OAUTH_SIGNUP_DISABLED`, `OAUTH_SIGNIN_DISABLED` | The project disabled OAuth signup or signin |
| `MISSING_OAUTH_CODE` | The provider did not return a code |
| `INVALID_CREDENTIALS`, `INVALID_TOKEN` | The account or linking session is invalid |
| `REQUIRE_PASSWORD_CHANGE` | The account must change its password first |
| `OAUTH_FAILED` | Unexpected failure |

### auth.oauth_signin

```json
{
  "operation": "auth.oauth_signin",
  "payload": {
    "access_code": "<ACCESS_CODE>",
    "nonce": "<NONCE>"
  }
}
```

The access code expires after 10 minutes and works once. Returns a session. For `intent: "link"`, the exchange also completes the link.

**Example - Browser flow:**
```javascript
// 1. Start
const { data: { nonce } } = await singlebase('auth.nonce');
sessionStorage.setItem('oauth_nonce', nonce);

const { data } = await singlebase('auth.oauth_connect', {
  provider: 'github',
  nonce,
  intent: 'signin'
});
window.location.href = data.oauth_redirect_url;

// 2. On your return URL
const params = new URLSearchParams(window.location.search);
const accessCode = params.get('access_code');

if (accessCode) {
  const session = await singlebase('auth.oauth_signin', {
    access_code: accessCode,
    nonce: sessionStorage.getItem('oauth_nonce')
  });
}
```

---

## Settings

### auth.settings

Read the project's public authentication settings to build your signin screens.

```json
{
  "operation": "auth.settings",
  "payload": {}
}
```

**Response:**
```json
{
  "data": {
    "enabled": true,
    "auth_settings": {
      "enabled": true,
      "allow_signin": true,
      "allow_signup": true,
      "signin_method": "password",
      "second_factor": null,
      "signup_verify_email": false,
      "password_policy": {}
    },
    "oauth_settings": {
      "enabled": true,
      "allow_signin": true,
      "allow_signup": true,
      "redirect_url": "https://app.example.com/auth/callback"
    },
    "oauth_providers": {
      "google": { "enabled": true, "name": "Google" }
    }
  }
}
```

Values are illustrative. Provider credentials are never returned.

---

## Current User

Use [the Current User API](/user.md) to read and update the account in your bearer token. It cannot select another user.

### user.get

See [user.get](/user.md#user-get) for the request and profile response.

### user.update

See [user.update](/user.md#user-update) for editable profile fields.

---

## Common Errors

| Message | Meaning |
|---|---|
| `EMAIL_SIGNUP_DISABLED`, `EMAIL_SIGNIN_DISABLED` | The project disabled this method |
| `INVALID_CREDENTIALS` | Wrong email, password, or code, or the account is inactive |
| `CODE_REQUIRED` | The project requires a code with the password |
| `REQUIRE_PASSWORD_CHANGE` | The account must reset its password before signing in |
| `INVALID_BEARER_TOKEN` | The ID token or its session is invalid |
| `AUTHENTICATION_REQUIRED` | The operation needs a bearer token |
| `AGENT_KEY_EXPIRED` | The agent key reached its expiry; returns 401 before scope checks or usage tracking. Extend it or remove its expiry through `console.update_agent_key` |
| `CUSTOM_SIGNUP_CLAIMS_NOT_ALLOWED` | Signup sent roles or a custom audience |
| `VERIFICATION_DISABLED` | Email codes are disabled for this change |
| `INVALID_NONCE` | The OAuth nonce is invalid, expired, or does not match |
| `MISSING_PROFILE_DATA` | `user.update` had no supported fields |

See [Responses](/api.md#responses) for the error envelope.

---

## Next Steps

- [Data](/data.md) - Store data owned by the signed-in user
- [Files](/files.md) - Let users upload files
- [Users](/users.md) - Manage users from your server
