# Connector specification

> The normative rules a OneiD connector for any framework, product, gateway or SaaS must follow, with a conformance checklist and tests.

Source: https://oltinid.com/docs/ai/connector-specification/ · Section: AI agents · All OneiD documentation: https://oltinid.com/llms.txt

This page specifies how to build a OneiD connector: an integration that lets a web framework, a product, an API gateway or a SaaS product sign users in with OneiD, call APIs with OneiD tokens, or accept OneiD tokens. It is written so that a developer or an AI coding agent can build and test a connector from this page alone. Where this page and general OpenID Connect habits differ, this page wins.

## 1. Scope and terms

### 1.1 Requirement words

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, RECOMMENDED, MAY and OPTIONAL are to be interpreted as described in RFC 2119 and RFC 8174 when, and only when, they appear in capitals.

### 1.2 Modes

A connector implements one or more of these modes:

| Mode | What the connector does | Sections |
|---|---|---|
| Sign-in (relying party) | Signs users in to an application through OneiD and keeps an application session. | 2 to 11, 14, 15 |
| Resource server | Accepts OneiD access tokens on an API and decides whether to serve the request. | 2, 3, 12, 14, 15 |
| Machine to machine | Gets access tokens for a service, without a user, to call APIs. | 2, 3, 13, 14, 15 |

### 1.3 Terms

| Term | Meaning |
|---|---|
| OneiD address | The base address of a OneiD deployment, for example `https://YOUR_ONEID`. |
| Issuer | The OneiD address with a trailing slash, `https://YOUR_ONEID/`. It is the `issuer` in discovery and the `iss` in every token. |
| Client | An application registered in OneiD. It has a client ID. |
| Public client | A client without a secret: browser, mobile or desktop applications. PKCE is always required. |
| Confidential client | A server-side client with one client secret. |
| Deployment | One OneiD installation: one issuer, one database, one user store. There are no tenants inside a deployment. |

### 1.4 Facts the connector must be built around

- Clients are registered by a OneiD administrator in the admin console or through the Admin API. There is no dynamic client registration. The connector MUST NOT try to register itself; it MUST let a person enter the values from section 2.
- Each customer of OneiD has its own deployment and therefore its own issuer. A connector used by several organisations (for example a SaaS product) MUST support one configuration per organisation, each with its own issuer, client and caches, and MUST keep their users apart.
- OneiD access tokens have no `aud` claim.
- The issuer ends with a slash.

## 2. Configuration inputs

The connector MUST accept these inputs. It MUST NOT hard-code any of them.

| Input | Required | Rules |
|---|---|---|
| OneiD address | Yes | An https address. The connector MUST accept it with or without the trailing slash and derive the issuer by ensuring exactly one trailing slash. |
| Client ID | Sign-in and machine-to-machine modes | As registered. |
| Client type | Sign-in mode | `public` or `confidential`. |
| Client secret | Confidential clients | MUST be read from a secret store or the environment, never from source code. MUST NOT be asked for or used by a public client. |
| Redirect URI | Sign-in mode | MUST equal a registered redirect URI exactly. |
| Post-logout redirect URI | Optional | MUST equal a registered post-logout redirect URI exactly. |
| Scopes | Sign-in and machine-to-machine modes | Space-separated. Sign-in default: `openid profile email`. `openid` MUST be included for sign-in. |
| Request refresh tokens | Optional | When on, the connector adds `offline_access`. Default off. |
| Response mode | Optional | `query` (default) or `form_post`. |
| Role claim name | Optional | Default `role`. |
| Role mapping | Optional | A table from OneiD role names to the product's own roles. |
| Required scopes | Resource-server mode | Per route or operation. |
| Allowed issuers | Resource-server mode | The issuers whose tokens are accepted. |
| Clock skew | Optional | Default 60 seconds. SHOULD NOT exceed 300 seconds. |
| Token endpoint authentication | Optional | `client_secret_basic` (default) or `client_secret_post`. |

The connector SHOULD check its configuration at start-up and report a clear error, for example when the redirect URI is not https and not a loopback address.

## 3. Discovery and keys

### 3.1 Discovery document

1. The connector MUST fetch the discovery document from `{issuer}.well-known/openid-configuration`, which is `https://YOUR_ONEID/.well-known/openid-configuration`.
2. The connector MUST check that the `issuer` in the document equals the issuer derived from the configured OneiD address, character for character. If not, it MUST stop and report a configuration error.
3. The connector MUST use the `issuer` value from the document, including the trailing slash, for every `iss` comparison.
4. The connector MUST take endpoint addresses from the document: `authorization_endpoint`, `token_endpoint`, `userinfo_endpoint`, `jwks_uri`, `end_session_endpoint`, `revocation_endpoint` and `introspection_endpoint`. It MUST NOT build them from the address.
5. The connector SHOULD cache the document, for example for 24 hours, and MUST NOT fetch it per request.

For reference, a OneiD discovery document looks like this (abridged):

```json
{
  "issuer": "https://YOUR_ONEID/",
  "authorization_endpoint": "https://YOUR_ONEID/connect/authorize",
  "token_endpoint": "https://YOUR_ONEID/connect/token",
  "userinfo_endpoint": "https://YOUR_ONEID/connect/userinfo",
  "jwks_uri": "https://YOUR_ONEID/.well-known/jwks",
  "end_session_endpoint": "https://YOUR_ONEID/connect/logout",
  "revocation_endpoint": "https://YOUR_ONEID/connect/revoke",
  "introspection_endpoint": "https://YOUR_ONEID/connect/introspect",
  "response_types_supported": ["code"],
  "response_modes_supported": ["form_post", "fragment", "query"],
  "grant_types_supported": ["authorization_code", "client_credentials", "refresh_token"],
  "subject_types_supported": ["public"],
  "id_token_signing_alg_values_supported": ["RS256"],
  "code_challenge_methods_supported": ["plain", "S256"],
  "token_endpoint_auth_methods_supported": ["client_secret_basic", "client_secret_post", "private_key_jwt"],
  "claims_parameter_supported": true,
  "request_parameter_supported": false,
  "request_uri_parameter_supported": false,
  "authorization_response_iss_parameter_supported": true,
  "acr_values_supported": ["urn:oltin:ac:pwd", "urn:oltin:ac:mfa", "urn:oltin:ac:external"]
}
```

Some advertised values MUST NOT be used: `plain` (section 4), `fragment` (section 4) and `private_key_jwt` (section 5).

### 3.2 Signing keys (JWKS)

1. The connector MUST cache the JWKS from `jwks_uri`.
2. When a token's `kid` is not in the cache, the connector MUST fetch the JWKS again once and retry the lookup. It MUST limit these re-fetches, for example to one per minute per issuer, so that tokens with made-up `kid` values cannot cause a fetch per request.
3. If the `kid` is still unknown, the connector MUST reject the token.
4. The connector MUST only use keys from the issuer's own `jwks_uri`.

OneiD signs with RSA keys. A new key is published in the JWKS before it starts signing; a retired key stays in the JWKS for 30 days. A connector that follows these rules keeps working through key rotation.

## 4. Sign-in

### 4.1 Authorisation request

The connector MUST use the authorization code flow with PKCE. It MUST send the user's browser to `authorization_endpoint` with these parameters:

| Parameter | Rule |
|---|---|
| `response_type` | MUST be `code`. |
| `client_id` | The configured client ID. |
| `redirect_uri` | The configured redirect URI, exactly as registered. |
| `scope` | The configured scopes, including `openid`. |
| `state` | MUST be sent. At least 128 bits from a cryptographically secure random generator, base64url-encoded. |
| `nonce` | MUST be sent. At least 128 bits from a cryptographically secure random generator. |
| `code_challenge` | MUST be sent. `BASE64URL(SHA256(code_verifier))` without padding. |
| `code_challenge_method` | MUST be `S256`. The connector MUST NOT use `plain`. |
| `response_mode` | OPTIONAL. `query` or `form_post`. The connector MUST NOT use `fragment`. |
| `prompt` | OPTIONAL. `none`, `login` or `consent`. See section 8. |
| `max_age` | OPTIONAL. See section 8. |
| `id_token_hint` | OPTIONAL. If it names a different user than the one signed in, OneiD asks for a new sign-in. |
| `claims` | OPTIONAL. OneiD reads claim names in the `id_token` and `userinfo` members and ignores `essential`, `value` and `values`. |

The `code_verifier` MUST be 43 to 128 characters from the unreserved set, generated from at least 256 bits of secure randomness.

The connector MUST store `state`, `nonce`, `code_verifier`, the redirect URI and the time of the request, bound to the user's browser (for example in the server-side session or in an encrypted, HttpOnly cookie). Each stored request MUST be usable once and SHOULD expire after a short time.

The connector MUST NOT send `request`, `request_uri`, `resource` or `audience`. `request` and `request_uri` are answered with `request_not_supported` and `request_uri_not_supported`; OneiD does not support resource indicators or an audience parameter. The connector MUST NOT rely on `login_hint`, `ui_locales`, `display` or `acr_values`: OneiD accepts them and they have no effect.

Example request:

```http
GET /connect/authorize?response_type=code
  &client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20profile%20email
  &state=Zk3q8xY2...&nonce=n0Q4r7...
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: YOUR_ONEID
```

### 4.2 Redirect URI rules

- OneiD compares redirect URIs exactly. There are no wildcards.
- Redirect URIs MUST use https, except loopback addresses (`localhost`, `127.0.0.1`, `[::1]`), which MAY use http.
- Private-use schemes in reverse-domain form (for example `com.example.app:/callback`) are allowed for public clients only.
- Redirect URIs MUST NOT contain a fragment.

If the `redirect_uri` is not registered or the `client_id` is unknown, OneiD does not redirect back. It shows an error page with `invalid_request`. The connector cannot detect this; it is a configuration error.

### 4.3 Callback

With `response_mode=query`, the callback receives a GET request with the response in the query. With `form_post`, it receives a POST with a form body. The connector MUST handle the mode it requested.

The connector MUST process the callback in this order:

1. Find the stored request by `state`. If `state` is missing, unknown, already used or expired, the connector MUST reject the response and MUST NOT call the token endpoint.
2. Check `iss`. OneiD includes `iss` in every authorisation response (RFC 9207). The connector MUST reject the response if `iss` is missing or does not equal the issuer exactly.
3. If the response contains `error`, handle it as in section 14 and stop.
4. Mark the stored request as used.
5. Exchange `code` at the token endpoint once (section 5).
6. Validate the ID token (section 6).
7. Create the application session (section 8) and redirect the user to the stored return address, which MUST be a local or allow-listed address.

With `form_post`, the browser sends the callback as a cross-site POST. A cookie that holds the stored request MUST be `SameSite=None; Secure` to be sent with it, or the connector MUST keep the stored request elsewhere.

## 5. Token request and client authentication

The connector MUST send a POST to `token_endpoint` with `Content-Type: application/x-www-form-urlencoded`.

| Parameter | Value |
|---|---|
| `grant_type` | `authorization_code` |
| `code` | The code from the callback |
| `redirect_uri` | The same redirect URI as in the authorisation request |
| `code_verifier` | The stored verifier |
| `client_id` | Public clients, and confidential clients using `client_secret_post` |

Client authentication:

- Confidential clients SHOULD use `client_secret_basic`: an `Authorization: Basic` header with `base64(urlencode(client_id) + ":" + urlencode(client_secret))`. They MAY use `client_secret_post`.
- Public clients MUST send `client_id` in the body and no secret.
- The connector MUST NOT use `private_key_jwt`. Discovery lists it, but there is no way to register a client's public key with OneiD. Mutual TLS client authentication is not supported.

```http
POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Authorization: Basic WU9VUl9DTElFTlRfSUQ6WU9VUl9DTElFTlRfU0VDUkVU
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=CODE_FROM_CALLBACK
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&code_verifier=VERIFIER_FROM_STEP_4
```

A successful response contains `access_token`, `token_type` (`Bearer`), `expires_in`, `id_token`, `scope`, and `refresh_token` when section 9 applies.

- Authorisation codes work once and expire after 5 minutes. The connector MUST NOT retry a code exchange that failed with `invalid_grant`; it MUST start a new sign-in.
- Authorisation codes and refresh tokens are opaque. The connector MUST NOT parse them.
- In sign-in mode, the connector SHOULD NOT read the user's identity from the access token. It MUST use the ID token.

## 6. ID token validation

The connector MUST validate every ID token in this order and reject it if any step fails:

1. The token is a signed JWT in compact form.
2. The header `alg` is `RS256`. Any other value, including `none`, is rejected.
3. The header `kid` names a key from the JWKS (section 3.2) and the signature verifies with it.
4. `iss` equals the issuer from discovery exactly, including the trailing slash.
5. `aud` equals the client ID, or is an array that contains only the client ID.
6. `exp` is later than the current time minus the clock skew.
7. `iat` is not later than the current time plus the clock skew.
8. `nonce` equals the stored nonce for this sign-in.
9. `sub` is present and not empty.
10. If `max_age` was sent, `auth_time` is present and the current time minus `auth_time` is no more than `max_age` plus the clock skew.
11. If the connector requires a level of authentication (section 8.4), `acr` or `amr` meets it.

The connector MUST ignore claims it does not know, including private claims whose names start with `oi_`.

The ID token is validated once, at sign-in. Its lifetime (20 minutes by default) is not the application session's lifetime. The connector MUST keep the raw ID token for sign-out (section 10).

Example ID token payload after an MFA sign-in with `openid profile email roles`:

```json
{
  "iss": "https://YOUR_ONEID/",
  "sub": "8d0c6a3e-2f4b-4c1e-9a77-1b2c3d4e5f60",
  "aud": "YOUR_CLIENT_ID",
  "exp": 1767226800,
  "iat": 1767225600,
  "nonce": "n0Q4r7...",
  "auth_time": 1767225590,
  "amr": ["pwd", "mfa"],
  "acr": "urn:oltin:ac:mfa",
  "name": "Jane Doe",
  "preferred_username": "jdoe",
  "email": "jane.doe@example.com",
  "email_verified": true,
  "idp": "local",
  "role": ["OrderViewer", "Support"]
}
```

## 7. User mapping

### 7.1 Key

The connector MUST identify a user by the pair (`iss`, `sub`). It MUST NOT use `email`, `preferred_username` or `name` as the key. `sub` is opaque and stable; the connector MUST NOT parse it.

### 7.2 Profile fields

| Claim | Scope | Product field | Notes |
|---|---|---|---|
| `sub` | `openid` | External user ID | With `iss`, the key. |
| `name` | `profile` | Display name | |
| `given_name`, `family_name` | `profile` | First and last name | |
| `preferred_username` | `profile` | User name | Display only. |
| `updated_at` | `profile` | Profile last changed | A number, seconds since 1970. |
| `idp` | `profile` | Sign-in source | `local`, `ldap` or `oidc`. |
| `email` | `email` | Email | Not a key. |
| `email_verified` | `email` | Email verified | Boolean. The connector MUST NOT treat the address as verified unless this is `true`. |
| `phone_number` | `phone` | Phone | |
| `phone_number_verified` | `phone` | Phone verified | Boolean. |
| `role` (or the configured name) | `roles` | Roles | See 7.3. |

Users can untick optional scopes on the consent page. The connector MUST work when any claim other than `sub` is missing. It SHOULD update stored profile fields at each sign-in.

The connector MAY call `userinfo_endpoint` with the access token (`GET` or `POST`, `Authorization: Bearer`). If it does, it MUST check that the `sub` in the response equals the ID token's `sub`. In userinfo, `role` is a JSON array.

### 7.3 Roles

- In tokens, the role claim can be a single string (one role) or an array (several roles). The connector MUST accept both.
- The connector SHOULD map OneiD role names to product roles through the configured role mapping, and SHOULD give no product role for an unmapped OneiD role.
- OneiD's built-in administrator roles (`All`, `AllReadOnly`, `UserManager`, `UserManagerReadOnly`, `AuthorizationServerManager`, `AuthorizationServerManagerReadOnly`, `Auditer`) control OneiD itself. The connector MUST NOT give them meaning in the product unless the person configuring it maps them explicitly.

## 8. Sessions

### 8.1 Application session

- After sign-in, the connector MUST create its own session (for example an HttpOnly, Secure cookie) and MUST issue a new session identifier at that moment.
- The connector cannot read OneiD's own session; it can only end it by sending the user to the end-session endpoint (section 10). OneiD keeps a browser session for 8 hours, extended while the user is active. Within it, other applications sign the user in without a password prompt.
- OneiD does not notify the connector when the user signs out elsewhere (no front-channel or back-channel logout). The application session lifetime MUST be configurable and SHOULD be short, for example no longer than OneiD's session.

### 8.2 Renewing a session without user interaction

The connector MAY renew a session by repeating the authorisation request with `prompt=none` as a top-level redirect. It MUST NOT use a hidden iframe: OneiD pages cannot be framed. OneiD then either returns a code without showing anything, or returns `login_required`, `consent_required` or `interaction_required`. On any of these errors the connector MUST end the application session or start an interactive sign-in. It MUST NOT retry `prompt=none` in a loop. Refresh tokens (section 9) are the alternative.

### 8.3 Forcing a new sign-in

- `prompt=login` (or `prompt=select_account`, which behaves the same; there is no account picker) forces the user to sign in again.
- `max_age=N` forces a new sign-in when the user signed in more than N seconds ago.
- After either, the connector SHOULD check `auth_time` in the new ID token.

### 8.4 Step-up and MFA

OneiD does not enforce `acr_values`. Asking for `urn:oltin:ac:mfa` does not make OneiD require MFA. The connector MUST make the check itself:

| `acr` | `amr` | Meaning |
|---|---|---|
| `urn:oltin:ac:pwd` | `["pwd"]` | Password only. |
| `urn:oltin:ac:mfa` | `["pwd","mfa"]` | Password and an authenticator code in OneiD. |
| `urn:oltin:ac:external` | `["external"]` | Signed in at an upstream OpenID Connect provider, whose own MFA policy applies. OneiD cannot tell whether MFA happened there. |

When an action requires MFA and the ID token does not show it, the connector MUST refuse the action and tell the user why. Whether `urn:oltin:ac:external` counts SHOULD be a configuration choice. To make MFA certain, the OneiD administrator can make MFA mandatory for users.

### 8.5 Pending actions

Users who must change their password or enrol MFA are sent to do that during interactive sign-in. The connector needs no handling beyond section 14.

## 9. Refresh tokens

1. The connector MUST request `offline_access` only when refresh tokens are switched on. The client must also have the refresh token grant; if no `refresh_token` is returned, the connector MUST work without one.
2. To refresh, the connector POSTs to `token_endpoint` with `grant_type=refresh_token` and `refresh_token`, authenticating as in section 5.
3. Every refresh returns a new refresh token. The connector MUST store the new one, replacing the old one, before it uses the new access token, and MUST NOT use the old one again.
4. The connector MUST allow at most one refresh at a time per refresh token. Concurrent requests that need a new access token MUST wait for the refresh in progress. A refresh token used again after a short grace period (about 30 seconds) is refused with `invalid_grant`, and OneiD then revokes the whole chain, including newer tokens.
5. The connector SHOULD refresh shortly before the access token expires, using `expires_in`, or once after an API answers 401.
6. Refresh tokens are not sliding. A chain ends when the original refresh token's lifetime runs out (14 days by default), however often it was used.
7. On `invalid_grant`, the connector MUST discard the stored tokens and require an interactive sign-in. Causes include an expired, revoked or reused token, and a user who was deleted or locked, or must change the password or enrol MFA.
8. If a refresh response contains an ID token, the connector MUST validate it as in section 6, except the `nonce` step, and MUST check that `sub` is unchanged.

Refresh tokens MUST be stored server-side, or in the operating system's secure storage on mobile and desktop. They SHOULD be encrypted at rest.

## 10. Sign-out

The connector MUST sign out in this order:

1. End the application session and delete stored tokens.
2. SHOULD revoke the refresh token, if any, with a POST to `revocation_endpoint` (`token`, `token_type_hint=refresh_token`, client authentication as in section 5).
3. Redirect the browser (GET, or POST with a form) to `end_session_endpoint` with:

| Parameter | Rule |
|---|---|
| `id_token_hint` | SHOULD be sent: the raw ID token from sign-in. |
| `post_logout_redirect_uri` | OPTIONAL. MUST equal a registered post-logout redirect URI exactly. |
| `client_id` | SHOULD be sent. REQUIRED with `post_logout_redirect_uri` when no `id_token_hint` is sent. |
| `state` | SHOULD be sent with `post_logout_redirect_uri`: a fresh random value. |

4. When the browser returns to `post_logout_redirect_uri`, the connector SHOULD check that `state` matches.

```http
GET /connect/logout?id_token_hint=eyJhbGciOiJSUzI1NiIs...
  &post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2F
  &state=af0ifjsldkj HTTP/1.1
Host: YOUR_ONEID
```

Behaviour the connector must expect:

- A `post_logout_redirect_uri` sent with neither `id_token_hint` nor `client_id` is refused with `invalid_request`.
- An `id_token_hint` that OneiD did not issue is refused.
- If the hint names a different user than the one signed in, OneiD asks the user to confirm.
- Without `post_logout_redirect_uri`, the user ends on OneiD's signed-out page.
- OneiD revokes the application's tokens for that user. If users sign in at an upstream provider, OneiD continues to the provider's sign-out before returning.
- Other applications are not notified. The connector MUST NOT expose or wait for front-channel or back-channel logout endpoints.

## 11. Calling APIs

- The connector MUST send the access token as `Authorization: Bearer <access_token>`. It MUST NOT send the ID token to APIs.
- API scopes (for example `orders.read`) are requested in the same `scope` parameter as identity scopes. One access token carries all granted scopes.
- Access tokens have no `aud`. The connector MUST NOT send `resource` or `audience` parameters to get a token for a particular API; OneiD does not support resource indicators.
- Access tokens live 1 hour by default. The connector SHOULD use `expires_in` from the token response rather than parse the token.
- On a 401 from an API, the connector MAY refresh once (section 9) and retry the request once.

## 12. Resource-server mode

### 12.1 Validation

For every request, the connector MUST:

1. Read the token from the `Authorization: Bearer` header. Tokens in query strings MUST NOT be accepted.
2. Parse the JWT header and check that `alg` is `RS256`.
3. Read `iss` from the unverified payload only to select a configuration. The issuer MUST be in the configured allowed issuers; otherwise reject. The connector MUST NOT fetch discovery or keys from an issuer that is not configured.
4. Verify the signature with that issuer's JWKS (section 3.2).
5. Check that `iss` equals the issuer exactly, with the trailing slash.
6. Check `exp`, and `nbf` if present, with the configured clock skew.
7. Check that the space-separated `scope` claim contains the scope the route requires.
8. Check the role claim if the route requires a role.

The connector MUST NOT require an `aud` claim. Because there is no audience, each API SHOULD have its own scopes so that a token issued for one API's scope is not accepted by another API.

### 12.2 Access token claims

| Claim | Meaning |
|---|---|
| `iss` | The issuer, with the trailing slash. |
| `sub` | The user's ID, or the client ID for client credentials tokens. |
| `exp`, `iat` | Expiry and issue time. |
| `scope` | Granted scopes, separated by spaces. |
| `client_id` | The client the token was issued to. |
| `role` | Roles, when `roles` was granted. String or array. |

Scope-released profile, email and phone claims can also be present. A token ID claim may be present. The connector MUST ignore unknown claims. Access tokens are signed, not encrypted.

### 12.3 Responses

| Situation | Status | `WWW-Authenticate` |
|---|---|---|
| No token | 401 | `Bearer` |
| Invalid, expired or untrusted token | 401 | `Bearer error="invalid_token"` |
| Required scope missing | 403 | `Bearer error="insufficient_scope", scope="<required scope>"` |
| Role missing | 403 | Not required |

### 12.4 Revocation

OneiD revokes tokens in its store, for example when a user signs out or an administrator locks the user. Local validation does not see this before the token expires. Introspection (`introspection_endpoint`) requires a confidential client and only answers for tokens issued to that same client, so it is not suited to general APIs. The RECOMMENDED pattern is local validation with short access token lifetimes.

## 13. Machine-to-machine mode

1. The client MUST be confidential and have the client credentials grant. Public clients cannot use it.
2. The connector POSTs to `token_endpoint` with `grant_type=client_credentials` and `scope`, authenticating with `client_secret_basic` (or `client_secret_post`).
3. The response contains `access_token`, `token_type`, `expires_in` and `scope`. There is no refresh token and no ID token.
4. The connector MUST cache the access token and reuse it until shortly before `expires_in` runs out. It MUST NOT request a token per API call, and SHOULD allow only one token request at a time per client and scope set.
5. The token's `sub` is the client ID and `name` is the client's display name.

```http
POST /connect/token HTTP/1.1
Host: YOUR_ONEID
Authorization: Basic WU9VUl9DTElFTlRfSUQ6WU9VUl9DTElFTlRfU0VDUkVU
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=orders.read
```

## 14. Errors to handle

| Where | Error | Cause | Connector behaviour |
|---|---|---|---|
| Callback | `access_denied` | The user refused consent. | Show that access was declined and offer to try again. Do not retry automatically. |
| Callback | `login_required`, `consent_required`, `interaction_required` | `prompt=none` needed interaction. | Start an interactive sign-in, or end the session. |
| Callback | `invalid_scope` | A scope is not allowed for the client. | Configuration error. Show a generic error and log the scope list. |
| Callback | `unauthorized_client` | The client is disabled. | Configuration error. Show a generic error. |
| Callback | `request_not_supported`, `request_uri_not_supported` | A request object was sent. | Connector defect. Do not send `request` or `request_uri`. |
| Callback | `invalid_request`, other errors | A malformed request. | Show a generic error and log `error` and `error_description`. |
| Callback | `state` or `iss` check failed | Forged or stale response. | Reject. Do not call the token endpoint. Offer a new sign-in. |
| OneiD error page | `invalid_request` shown by OneiD | Unknown client ID or unregistered redirect URI. | Not visible to the connector. Check configuration. |
| Token endpoint | `invalid_grant` | Code or refresh token expired, used or revoked; PKCE mismatch; user deleted, locked, must change the password or must enrol MFA. | Do not retry with the same value. Start a new sign-in. |
| Token endpoint | `invalid_client` | Wrong secret, expired secret ("The client secret has expired."), or client disabled. | Configuration error. Alert the operator. Do not retry in a loop. |
| Token endpoint | `invalid_scope` | A scope is not allowed for the client. | Configuration error. |
| Logout | `unauthorized_client` | The client is disabled. | Shown by OneiD; the connector does not see it. Local sign-out is already done. Check configuration. |
| Any endpoint | HTTP 429, `slow_down` | Rate limit reached. | Wait the number of seconds in `Retry-After`, then retry at most once. Never retry in a tight loop. |
| Userinfo, APIs | HTTP 401 | Access token expired or revoked. | Refresh once (section 9), then sign in again. |
| Any endpoint | Network error or HTTP 5xx | Temporary failure. | Retry back-channel calls with exponential back-off. Do not retry a code exchange. |

A 429 response looks like this:

```http
HTTP/1.1 429 Too Many Requests
Retry-After: 12
Content-Type: application/json

{"error":"slow_down","error_description":"Too many requests. Try again in 12 seconds."}
```

## 15. Security requirements

The connector MUST:

- use TLS for every call to OneiD and MUST NOT disable certificate validation;
- never write tokens, authorisation codes, `code_verifier` values or client secrets to logs, URLs it builds, error pages, analytics or crash reports;
- keep client secrets out of browser bundles, mobile apps and desktop apps;
- protect the callback against cross-site request forgery with `state`, bound to the browser and used once;
- check `iss` in the authorisation response and in every token;
- set session cookies `HttpOnly` and `Secure`, with `SameSite=Lax` or stricter except where section 4.3 requires `None`;
- issue a new session identifier at sign-in;
- validate any return address after sign-in or sign-out against local or allow-listed addresses;
- keep tokens server-side in server-side applications, in memory in browser applications, and in secure storage on devices;
- keep configurations for different issuers fully separate, including caches and user records.

For browser-based connectors: the token, userinfo and revocation endpoints accept cross-origin requests only from origins an administrator registered for an enabled client. Introspection never accepts them. The connector MUST NOT rely on cookies in cross-origin calls.

## 16. Not supported by OneiD

The connector MUST NOT depend on any of the following:

- Dynamic client registration, or any self-service registration of clients or users.
- Implicit and hybrid flows; any `response_type` other than `code`.
- The `fragment` response mode (listed in discovery, not recommended).
- PKCE `plain`.
- The resource owner password credentials grant, device authorization grant, token exchange and CIBA.
- Pushed authorisation requests (PAR) and request objects (JAR, `request`, `request_uri`).
- DPoP, mutual TLS client authentication and certificate-bound tokens.
- `private_key_jwt` client authentication.
- Resource indicators (`resource`) and an `audience` parameter; an `aud` claim in access tokens.
- Front-channel logout, back-channel logout and the session management iframe (`check_session_iframe`); the `sid` claim.
- Enforcement of `login_hint`, `ui_locales`, `display` and `acr_values`.
- `essential`, `value` and `values` in the `claims` parameter.
- The `address` scope.
- An account picker for `prompt=select_account`.
- SAML, WS-Federation and SCIM.
- Outgoing event webhooks from OneiD.
- Social-login buttons, passkeys or WebAuthn, and SMS codes.

## 17. Conformance checklist

Tick every item that applies to the modes the connector implements. Each test SHOULD be automated. For unit tests, run a local fake OneiD: serve the discovery document from section 3.1 and a JWKS from a test RSA key, and sign test tokens with it. For integration tests, ask your OneiD administrator to register a test client.

### 17.1 Configuration and discovery

- [ ] All inputs from section 2 are configurable; nothing is hard-coded.
- [ ] The issuer is derived with exactly one trailing slash and compared exactly with discovery.
- [ ] Endpoints come from discovery.
- [ ] Discovery and JWKS are cached; an unknown `kid` causes one rate-limited re-fetch.
- [ ] Several issuers can be configured side by side (multi-organisation connectors).

### 17.2 Sign-in

- [ ] Authorization code flow with PKCE `S256`, `state` and `nonce` on every request.
- [ ] The redirect URI is sent exactly as registered.
- [ ] `iss` in the authorisation response is required and checked.
- [ ] The ID token is validated with every step of section 6.
- [ ] Users are keyed by (`iss`, `sub`).
- [ ] Missing optional claims do not break sign-in.
- [ ] The role claim is accepted as a string or an array.
- [ ] `amr`/`acr` checks are applied where the product requires MFA.

### 17.3 Tokens and sign-out

- [ ] Confidential clients use `client_secret_basic` by default; public clients send no secret.
- [ ] Refresh tokens are rotated, stored and never reused; refreshes are serialised.
- [ ] `invalid_grant` on refresh ends the session.
- [ ] Sign-out clears the local session, revokes the refresh token and redirects with `id_token_hint`, `post_logout_redirect_uri` and `state`.

### 17.4 Resource server and machine to machine

- [ ] `aud` is not required; `iss`, signature, `exp` and scope are.
- [ ] Only configured issuers are accepted.
- [ ] 401 and 403 responses carry the `WWW-Authenticate` values from section 12.3.
- [ ] Client credentials tokens are cached until shortly before expiry.

### 17.5 Security

- [ ] No token, code, verifier or secret appears in logs, URLs or error pages.
- [ ] Session cookies are `HttpOnly` and `Secure`; a new session ID is issued at sign-in.
- [ ] Return addresses are validated.

### 17.6 Tests

| Test | Setup | Expected result |
|---|---|---|
| Happy-path sign-in | Complete a sign-in. | Session created for (`iss`, `sub`); profile fields filled. |
| Wrong `state` | Change `state` on the callback. | Rejected; no token request made. |
| Replayed callback | Send the same callback twice. | Second one rejected. |
| Missing or wrong `iss` in the callback | Remove `iss`, or set another issuer. | Rejected. |
| Nonce mismatch | ID token with another `nonce`. | Rejected. |
| Expired ID token | `exp` earlier than now minus the skew. | Rejected. |
| Wrong algorithm | ID token with `alg` `none` or `HS256`. | Rejected. |
| Wrong audience | ID token `aud` is another client ID. | Rejected. |
| Issuer without slash | Token `iss` is `https://YOUR_ONEID`. | Rejected. |
| Key rotation | Token signed with a new key that is added to the JWKS after the first fetch. | One JWKS re-fetch, then accepted. |
| Unknown key flood | Many tokens with random `kid` values. | Rejected; JWKS re-fetches stay within the limit. |
| Refresh rotation | Refresh twice. | Each response's new refresh token is stored; the old one is never sent again. |
| Concurrent refresh | Two requests need a refresh at the same time. | One refresh request reaches OneiD. |
| Refresh failure | Token endpoint answers `invalid_grant`. | Tokens discarded; user must sign in. |
| Sign-out round trip | Sign out. | Local session gone; redirect carries `id_token_hint`, `post_logout_redirect_uri` and `state`; `state` checked on return. |
| Consent refused | Callback with `access_denied`. | Message shown; no automatic retry. |
| Silent sign-in fails | Callback with `login_required` after `prompt=none`. | Interactive sign-in or session end; no loop. |
| Rate limited | Token endpoint answers 429 with `Retry-After: 2`. | Waits at least 2 seconds before one retry. |
| API without token | Call a protected route without a token. | 401 with `WWW-Authenticate: Bearer`. |
| API with expired token | Expired access token. | 401 with `error="invalid_token"`. |
| API without scope | Valid token without the route's scope. | 403 with `error="insufficient_scope"`. |
| API token without `aud` | Valid token with the scope and no `aud`. | Accepted. |
| API with unknown issuer | Token from an issuer that is not configured. | Rejected without any network call to that issuer. |
| Client credentials caching | Make several API calls. | One token request until shortly before expiry. |
| Log hygiene | Run all tests with debug logging. | No token, code, verifier or secret in the logs. |

## Learn more

- [Build with AI coding agents](https://oltinid.com/docs/ai/build-with-ai/)
- [Protect an API (validate access tokens)](https://oltinid.com/docs/guides/protect-an-api/)
- [Standards support](https://oltinid.com/docs/reference/standards-support/)
