---
title: "oauthsonas configuration"
description: "Configure oauthsonas clients, personas, claim names, TTLs, and CORS. Strict YAML decoding and validation with --check-config."
canonical_url: https://optimi.com/en/docs/oauthsonas/configuration
md_url: https://optimi.com/en/docs/oauthsonas/configuration.md
last_updated: 2026-09-04
---

# oauthsonas configuration

YAML configuration is decoded **strictly**: unknown fields fail startup rather
than being ignored. Validate without starting the server:

```sh
go run ./cmd/oauthsonas --config config.yaml --check-config
```

The repository ships [`config.example.yaml`](https://github.com/optimiweb/oauthsonas/blob/main/config.example.yaml)
with a public `dashboard` client and platform, staff, and customer personas.

## Top-level fields

| Field | Purpose |
| --- | --- |
| `issuer` | Exact OIDC issuer URL (default example: `http://127.0.0.1:8181`) |
| `api_audience` | Audience stamped on access tokens |
| `token_ttl` | Access (and related) token lifetime |
| `authorization_code_ttl` | Authorization code lifetime |
| `refresh_token_ttl` | Refresh token lifetime |
| `claims` | Optional claim name overrides (see below) |
| `clients` | Registered OAuth clients |
| `personas` | Selectable identities on the authorize page |
| `allowed_roles` | Optional vocabulary; when set, persona roles must be members |

## Clients

```yaml
clients:
  - id: dashboard
    name: Optimi Dashboard
    public: true
    redirect_uris:
      - http://127.0.0.1:5173/auth/callback
    post_logout_redirect_uris:
      - http://127.0.0.1:5173/
    allowed_origins:
      - http://127.0.0.1:5173
```

Rules:

- `redirect_uris` and `post_logout_redirect_uris` are **exact-match** values.
  Fixed query parameters are allowed; fragments are not.
- `allowed_origins` permits browser CORS only for the listed origins.
- Token and userinfo responses apply client-specific CORS; discovery and JWKS
  apply CORS only to the **union** of registered origins — never `*`.
- Public clients (as in the example) require **S256 PKCE**.

## Personas

Personas are the identities a developer picks on the authorize page. Each needs
a unique `id` and at least one role:

```yaml
personas:
  - id: acme-admin
    subject: oauthsonas|acme-admin
    email: admin@acme.dev.optimi.test
    name: Acme Administrator
    organization_id: org_acme
    roles: [customer-admin]

  - id: platform-admin
    subject: oauthsonas|platform-admin
    email: platform-admin@dev.optimi.test
    name: Platform Admin
    roles: [platform-admin]
```

| Field | Required | Notes |
| --- | --- | --- |
| `id` | yes | Stable selector / form value |
| `subject` | yes | JWT `sub` |
| `email` | yes | Emitted when `email` scope is granted |
| `name` | yes | Emitted when `profile` scope is granted |
| `roles` | yes | At least one role name |
| `organization_id` | no | Becomes `org_id` (or configured claim) when set |
| `memberships` | no | Optional multi-org membership list |

Roles are carried as **names only**. oauthsonas never expands roles into
application permissions — that stays in the relying party.

Add a persona by appending a unique entry under `personas`. Optionally set a
top-level `allowed_roles` list if a project wants configuration-time vocabulary
validation; otherwise any non-empty role name is valid.

## Application claims

Persona attributes are emitted as JWT and userinfo claims. Defaults:

| Persona field | Default claim name | When emitted |
| --- | --- | --- |
| `roles` | `roles` | Always (at least one role required) |
| `organization_id` | `org_id` | Only when set on the persona |
| `memberships` | `memberships` | Only when set on the persona |

Example access-token payload with defaults:

```json
{
  "roles": ["customer-admin"],
  "org_id": "org_acme"
}
```

`org_id` is omitted for staff personas without `organization_id`.
`memberships` is omitted unless the persona lists them.

### Overriding claim names

```yaml
claims:
  roles: roles
  memberships: memberships
  org_id: org_id
```

Use namespaced URLs when a consumer requires Auth0-style custom claims:

```yaml
claims:
  roles: https://example.com/roles
  memberships: https://example.com/memberships
  org_id: org_id
```

Rules:

- Claim names must be non-empty and must not contain whitespace.
- The three configured names must be distinct from each other.
- The same names appear in access tokens, ID tokens, userinfo, and discovery
  `claims_supported`.
- Persona YAML fields (`roles`, `organization_id`, `memberships`) stay the same;
  only the emitted JWT claim **keys** change.
- Custom claim names must not collide with standard JWT/OIDC claim names
  (for example `sub`, `email`, `scope`, `client_id`).

## Scope-gated profile claims

- `email` and `email_verified` require the `email` scope.
- `name` requires `profile`.
- OAuth scopes remain protocol scopes and do not encode application permissions.

## Keys and token shape

The server generates a **2048-bit RSA** key when it starts and publishes the
public component at `/.well-known/jwks.json`. Access and ID tokens are signed
with **RS256** and include a process-lifetime `kid`. Neither unsigned nor
symmetric JWTs are produced.

JWT `aud` values are serialized as JSON arrays by Fosite (valid JWT/OIDC):

- Access tokens target the configured `api_audience`.
- ID tokens target the client ID.

## Example: full minimal config

```yaml
issuer: http://127.0.0.1:8181
api_audience: https://api.example.test
token_ttl: 15m
authorization_code_ttl: 5m
refresh_token_ttl: 8h

clients:
  - id: dashboard
    name: Local Dashboard
    public: true
    redirect_uris:
      - http://127.0.0.1:5173/auth/callback
    post_logout_redirect_uris:
      - http://127.0.0.1:5173/
    allowed_origins:
      - http://127.0.0.1:5173

personas:
  - id: developer
    subject: oauthsonas|developer
    email: dev@example.test
    name: Local Developer
    roles: [customer-admin]
    organization_id: org_local
```

## Next

- Back to **[oauthsonas](/en/docs/oauthsonas)** for protocol, containers, and
  automation.
- **[Open source overview](/en/docs/open-source)** for other projects.
