# Authentication

Replace the development token from [Quick start](quick-start.md) with a production setup: your own token endpoint, signed tokens, and permission profiles.

If you want a working example first, start with Quick start.

> **Note**
>
> **Prerequisites**
>
> You need two values from your environment in the [Customer Portal](https://portal.ckeditor.com/):
>
> * **The environment ID.** It goes into the `aud` claim of every token.
> * **An access key.** It signs the tokens. To create one, see [Access key](../../developer-resources/security/access-key.md).

<a id="how-tokens-work">

## How tokens work

Every request to CKEditor AI carries a JSON Web Token (JWT) as a bearer token, signed with your environment’s access key. The payload carries the claims that matter here:

* `aud` is your environment ID, exactly as it appears in the Customer Portal.
* `sub` is the ID of the user making the call.
* `exp` keeps the token short-lived, minutes to hours.
* `auth.ai.permissions` lists the user’s scopes. A misspelled scope is not rejected when the token is signed, and it grants nothing.

<a id="build-a-token-endpoint">

## Build a token endpoint

Your application issues the tokens. It authenticates the user, signs a short-lived JWT with your environment’s access key, and returns it to the client. [Token endpoint](../../developer-resources/security/token-endpoint.md) covers how to build one, and there is working code for [Node.js](../../examples/token-endpoints/nodejs.md), [Python](../../examples/token-endpoints/python.md), [Java](../../examples/token-endpoints/java.md), [PHP](../../examples/token-endpoints/php.md), and [ASP.NET](../../examples/token-endpoints/dotnet.md).

CKEditor AI adds one claim to that token, `auth.ai.permissions`. A payload for a user of the editor features looks like this:

```json
{
	"aud": "<your environment ID>",
	"sub": "<your user ID>",
	"iat": 1735689600,
	"exp": 1735693200,
	"auth": {
		"ai": {
			"permissions": [
				"ai:conversations:read",
				"ai:conversations:write",
				"ai:models:agent"
			]
		}
	}
}
```

Clients fetch a fresh token whenever they need one, so the short expiry costs them nothing.

<a id="set-the-user-permissions">

## Set the user permissions

Permissions are an array of scopes. The scopes decide which features, models, and contexts the user can call with the token. Each scope is a string of the form `ai:<type>:<value>`. See [Permissions](permissions.md) for the full catalog, wildcard rules, and error responses.

Most integrations need one of three profiles:

**End user:** a person who uses AI features through your UI or the editor.

```json
[
	"ai:conversations:read",
	"ai:conversations:write",
	"ai:actions:*",
	"ai:reviews:*",
	"ai:models:agent"
]
```

**Server-to-server:** a backend job that edits documents with no user present.

```json
[
	"ai:documents:process",
	"ai:models:agent"
]
```

**Admin:** an operator who manages contexts, MCP servers, and other environment-level resources. The `ai:admin` scope satisfies every permission check.

```json
[
	"ai:admin"
]
```

<a id="rotate-keys">

## Rotate keys

An environment can hold several access keys at once, and a token signed with any of them is valid. To rotate a key:

1. Create the new access key in the Customer Portal.
2. Switch your token endpoint to the new key.
3. Wait until the tokens signed with the old key have expired.
4. Remove the old key.

See [Access key](../../developer-resources/security/access-key.md) for adding and removing keys in the Customer Portal.

<a id="test-a-token">

## Test a token

Before you connect the token endpoint to your application, check that it works. These checks catch the most common mistakes.

<a id="read-the-claims">

### Read the claims

A JWT is not encrypted, so you can decode it and look at its contents. Paste it into [jwt.io](https://jwt.io/) or run `jwt decode <token>` locally, and check the payload against the claims in [How tokens work](#how-tokens-work). The scopes are the usual mistake: `auth.ai.permissions` has to hold the ones you expect, spelled correctly.

<a id="call-the-service">

### Call the service

Call the service with the token. Use the host for your environment’s region: `https://ai.cke-cs.com` for the US and `https://ai.cke-cs-eu.com` for the EU. Request the list of models first, because it has no LLM cost:

```bash
TOKEN=$(curl -s "<your-token-endpoint-url>")

# The US region host. For the EU region, use https://ai.cke-cs-eu.com
curl -s "https://ai.cke-cs.com/v1/models/1" \
	-H "Authorization: Bearer $TOKEN"
```

Each model in the response has an `allowed` flag. The flag shows whether your token carries the `ai:models:*` scope for that model:

```json
{
	"items": [
		{
			"id": "agent-1",
			"name": "Agent",
			"allowed": true
		}
	]
}
```

If a model you expected to use returns `"allowed": false`, the token is valid but does not have that model’s scope. See [Models](models.md) for the full response.

<a id="when-a-scope-is-missing">

### When a scope is missing

An endpoint returns `403` when your token does not carry a scope the endpoint needs. The response lists the missing scopes:

```json
{
	"code": "missing-permissions",
	"message": "The user is missing permissions",
	"data": {
		"missingPermissions": [ "ai:models:agent" ]
	}
}
```

Add the listed scopes to the `auth.ai.permissions` claim in your token endpoint, issue a new token, and send the request again.

<a id="next-steps">

## Next steps

* **[Permissions](permissions.md)** lists every scope, the wildcard rules, and the error responses.
* **[Security & compliance](security-and-compliance.md)** covers data flow, storage and retention, content moderation, and compliance attestations.

---

Full index of the Cloud Services documentation: [llms.txt](../../../llms.txt)
