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

# Authentication

> Principal types, scopes, and token lifecycle.

<Note>
  See the [Authentication API reference](/api-reference/authentication) for the interactive playground.
</Note>

## Principal types

DynamoSQL recognizes two principal types:

| Type            | Description                                              | How to authenticate                                    |
| --------------- | -------------------------------------------------------- | ------------------------------------------------------ |
| **Portal user** | Human user who logged in via the DynamoSQL portal        | Passkey or password + TOTP; portal issues a session    |
| **API client**  | Machine-to-machine service account created in the portal | `POST /v1/auth/token` with `clientId` + `clientSecret` |

The `/v1/query` endpoint is intended for API clients. Portal users typically interact through the portal's SQL editor.

## Getting a token

API clients authenticate by posting their credentials to the token endpoint:

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.dynamosql.com/v1/auth/token \
    -H "Content-Type: application/json" \
    -d '{
      "clientId": "YOUR_CLIENT_ID",
      "clientSecret": "YOUR_CLIENT_SECRET"
    }'
  ```

  ```python Python theme={null}
  import requests

  resp = requests.post(
      "https://api.dynamosql.com/v1/auth/token",
      json={
          "clientId": "YOUR_CLIENT_ID",
          "clientSecret": "YOUR_CLIENT_SECRET",
      },
  )
  data = resp.json()["data"]
  access_token = data["accessToken"]
  refresh_token = data["refreshToken"]
  ```

  ```javascript Node.js theme={null}
  const resp = await fetch("https://api.dynamosql.com/v1/auth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: "YOUR_CLIENT_ID",
      clientSecret: "YOUR_CLIENT_SECRET",
    }),
  });
  const { data } = await resp.json();
  const { accessToken, refreshToken } = data;
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch("https://api.dynamosql.com/v1/auth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      clientId: "YOUR_CLIENT_ID",
      clientSecret: "YOUR_CLIENT_SECRET",
    }),
  });
  const { data } = (await resp.json()) as {
    data: { accessToken: string; refreshToken: string; expiresIn: number };
  };
  ```
</CodeGroup>

The response contains:

```json theme={null}
{
  "success": true,
  "data": {
    "accessToken": "eyJhbGciOiJSUzI1NiIs...",
    "refreshToken": "eyJjdHkiOiJKV1QiLCJl...",
    "expiresIn": 3600,
    "tokenType": "Bearer"
  }
}
```

* **`accessToken`** -- JWT valid for one hour. Pass as `Authorization: Bearer <accessToken>`.
* **`refreshToken`** -- use to obtain a new access token without re-sending credentials.
* **`expiresIn`** -- token lifetime in seconds.

## Scopes

Scopes control which endpoints an API client can access. They are assigned when the client is created in the portal.

| Scope           | Required for                                                                                                                                                     |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `query`         | `POST /v1/query` (both `execute` and `plan` modes)                                                                                                               |
| `schemas:read`  | `GET /v1/schemas`, `GET /v1/schemas/{name}`                                                                                                                      |
| `schemas:write` | `POST /v1/schemas`, `PATCH /v1/schemas/{name}`, `DELETE /v1/schemas/{name}`, `POST /v1/schemas/{name}/refresh-metadata`, `POST /v1/schemas/{name}/validate-role` |
| `usage:read`    | `GET /v1/usage/summary`                                                                                                                                          |

New API clients receive `query` and `schemas:read` by default.

## Passing the bearer token

Include the token in the `Authorization` header on every request:

```
Authorization: Bearer YOUR_ACCESS_TOKEN
```

<Snippet file="auth-header.mdx" />

## Token refresh

When your access token expires, exchange the refresh token for a new access token instead of re-authenticating with credentials:

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST https://api.dynamosql.com/v1/auth/refresh \
    -H "Content-Type: application/json" \
    -d '{
      "refreshToken": "YOUR_REFRESH_TOKEN"
    }'
  ```

  ```python Python theme={null}
  resp = requests.post(
      "https://api.dynamosql.com/v1/auth/refresh",
      json={"refreshToken": refresh_token},
  )
  data = resp.json()["data"]
  access_token = data["accessToken"]
  ```

  ```javascript Node.js theme={null}
  const resp = await fetch("https://api.dynamosql.com/v1/auth/refresh", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken }),
  });
  const { data } = await resp.json();
  const { accessToken } = data;
  ```

  ```typescript TypeScript theme={null}
  const resp = await fetch("https://api.dynamosql.com/v1/auth/refresh", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ refreshToken }),
  });
  const { data } = (await resp.json()) as {
    data: { accessToken: string; expiresIn: number };
  };
  ```
</CodeGroup>

The refresh response contains a new `accessToken` and `expiresIn`. No new refresh token is issued.

## Token caching example

```python theme={null}
import requests
import time

class TokenCache:
    def __init__(self, client_id, client_secret):
        self.client_id = client_id
        self.client_secret = client_secret
        self._access_token = None
        self._refresh_token = None
        self._expires_at = 0

    def get(self):
        if time.time() < self._expires_at - 30:  # 30s buffer
            return self._access_token

        # Try refresh first if we have a refresh token
        if self._refresh_token:
            try:
                return self._refresh()
            except Exception:
                pass  # Fall through to full auth

        return self._authenticate()

    def _authenticate(self):
        resp = requests.post(
            "https://api.dynamosql.com/v1/auth/token",
            json={
                "clientId": self.client_id,
                "clientSecret": self.client_secret,
            },
        )
        resp.raise_for_status()
        data = resp.json()["data"]
        self._access_token = data["accessToken"]
        self._refresh_token = data.get("refreshToken")
        self._expires_at = time.time() + data["expiresIn"]
        return self._access_token

    def _refresh(self):
        resp = requests.post(
            "https://api.dynamosql.com/v1/auth/refresh",
            json={"refreshToken": self._refresh_token},
        )
        resp.raise_for_status()
        data = resp.json()["data"]
        self._access_token = data["accessToken"]
        self._expires_at = time.time() + data["expiresIn"]
        return self._access_token
```

## Tenant scoping

Every JWT carries a `tenantId` claim. The server reads this claim and scopes all DynamoDB access to that tenant's tables. You cannot query another tenant's data.

The `tenantId` field in the request body is optional. If provided, it must match the `tenantId` in the JWT -- a mismatch returns `403`.
