> For the complete documentation index, see [llms.txt](https://docs.inrupt.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.inrupt.com/ess/3.0/services/service-platform-management/token-exchange.md).

# Token Exchange

{% hint style="success" %}
Added in version 3.0.0
{% endhint %}

The Platform Management service hosts a token exchange endpoint that enables ESS to trust external Identity Providers (IdPs) directly, mapping their identifiers to internal ESS identifiers. This eliminates the need for the ESS OpenID Broker as an intermediary, allowing clients to authenticate with their existing IdP and exchange the resulting token for an ESS Access Token.

## Overview

ESS supports native Identity Provider integration. Rather than routing all authentication through the ESS Solid OIDC Broker, clients can:

1. Authenticate directly with an external OIDC-compliant Identity Provider (e.g., Okta, Azure AD, Ping Identity)
2. Exchange the IdP's token for an ESS Access Token via the Platform Management service's token exchange endpoint
3. Use the ESS Access Token to interact with ESS services

The token exchange endpoint implements [RFC 8693 (OAuth 2.0 Token Exchange)](https://datatracker.ietf.org/doc/html/rfc8693) and issues [RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068)-compliant JWT access tokens.

{% hint style="info" %}
If your deployment requires Solid-OIDC protocol compliance for interoperability with other Solid servers, ESS also supports authentication via the [Solid OIDC Broker](/ess/3.0/services/advanced-configuration/service-oidc.md). This is an advanced configuration not needed for standard enterprise deployments.
{% endhint %}

## Benefits

* **Simplified Integration**: Developers integrate against existing enterprise identity systems rather than building parallel authentication infrastructure
* **Multiple IdP Support**: A single IdP is recommended, but multiple IdPs can be configured if needed; new IdPs can be added or modified post-deployment without system redeployment
* **Enhanced Security**: Short-lived access tokens with a default 5-minute TTL and full audit trail on token exchange events
* **Regulatory Compliance**: Short token lifetimes meet regulated industry security standards

## Base URL

The token exchange endpoint runs on the Platform Management service:

```none
https://platform.<ESS Domain>
```

## Authentication Flow

The following describes the authentication flow for a client using the token exchange endpoint:

```
┌────────┐       ┌──────────┐       ┌─────────────────┐       ┌─────────────┐
│ Client │──1──▶ │   IdP    │       │    Platform     │       │ ESS Service │
│        │◀──2── │          │       │   Management    │       │             │
│        │──────────────3──────────▶│                 │       │             │
│        │◀─────────────4──────────│                 │       │             │
│        │──────────────────────────────────5───────────────▶│             │
└────────┘       └──────────┘       └─────────────────┘       └─────────────┘
```

1. Client authenticates with the external Identity Provider
2. IdP returns an ID token
3. Client sends the ID token to the Platform Management service's token exchange endpoint
4. The endpoint validates the ID token, maps the identity to ESS internal identifiers, and returns an ESS Access Token
5. Client uses the ESS Access Token to interact with ESS services

## Token Exchange Endpoint

### Endpoint Details

| Field        | Value                                            |
| ------------ | ------------------------------------------------ |
| Endpoint     | **`https://platform.{ESS Domain}/access/token`** |
| Method       | **`POST`**                                       |
| Content-Type | **`application/x-www-form-urlencoded`**          |

### Request Parameters

| Parameter                | Required | Description                                                                                                                                                                                                                                                                                |
| ------------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **`grant_type`**         | Yes      | Must be **`urn:ietf:params:oauth:grant-type:token-exchange`**                                                                                                                                                                                                                              |
| **`subject_token`**      | Yes      | The token from the external Identity Provider                                                                                                                                                                                                                                              |
| **`subject_token_type`** | No       | The token type (e.g., **`urn:ietf:params:oauth:token-type:id_token`**). ESS determines the token type by validating the token directly against the IdP's JWKS, so this parameter can be safely omitted. Accepted for [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) compliance. |
| **`audience`**           | No       | Target audience for the issued token. If not provided, the configured default audiences are used.                                                                                                                                                                                          |

### Example Request

```http
POST /access/token HTTP/1.1
Host: platform.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=urn:ietf:params:oauth:grant-type:token-exchange&subject_token=<idp-token>&subject_token_type=urn:ietf:params:oauth:token-type:id_token
```

### Success Response

```http
HTTP/1.1 200 OK
Content-Type: application/json

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "urn:ietf:params:oauth:token-type:access_token",
  "expires_in": 300
}
```

The issued ESS Access Token is a JWT ([RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068)) containing:

| Claim           | Description                                                                     |
| --------------- | ------------------------------------------------------------------------------- |
| **`iss`**       | The Platform Management service URL                                             |
| **`sub`**       | The ESS agent identifier (mapped from the external identity)                    |
| **`aud`**       | The target audience(s) — ESS service URLs that accept this token                |
| **`client_id`** | The ESS client identifier (mapped from the external `azp` or `client_id` claim) |
| **`jti`**       | Unique token identifier                                                         |
| **`exp`**       | Expiration time                                                                 |
| **`iat`**       | Issued-at time                                                                  |
| **`scope`**     | Token scope (if configured)                                                     |

### Error Responses

Error responses use the [OAuth 2.0 error format (RFC 6749)](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2):

```json
{
  "error": "<error_code>",
  "error_description": "<human-readable description>"
}
```

| Status    | Error Code            | Condition                                                                                   |
| --------- | --------------------- | ------------------------------------------------------------------------------------------- |
| **`400`** | **`invalid_grant`**   | The **`grant_type`** parameter is not **`urn:ietf:params:oauth:grant-type:token-exchange`** |
| **`400`** | **`invalid_request`** | The **`subject_token`** parameter is missing                                                |
| **`400`** | **`invalid_token`**   | The subject token is invalid, expired, or from an untrusted IdP                             |
| **`500`** | **`server_error`**    | An unexpected internal error occurred                                                       |

## JWKS Endpoint

The Platform Management service exposes a JSON Web Key Set (JWKS) endpoint that returns the public key used to verify ESS Access Tokens.

| Field    | Value                                    |
| -------- | ---------------------------------------- |
| Endpoint | **`https://platform.{ESS Domain}/jwks`** |
| Method   | **`GET`**                                |

ESS services and clients can use this endpoint to obtain the public key for verifying the signature of ESS Access Tokens.

## Security

### Short-Lived Tokens

ESS Access Tokens issued by the token exchange endpoint have a default time-to-live (TTL) of 5 minutes (300 seconds). This limits the window of exposure if a token is compromised.

### Token Refresh

The token exchange endpoint does not issue refresh tokens. When an ESS Access Token expires, clients must obtain a new one by performing another token exchange with a valid IdP token.

In practice:

1. **Track expiry**: Use the `expires_in` value from the token exchange response to know when the token will expire.
2. **Obtain a new IdP token**: Re-authenticate with your Identity Provider. If your IdP issued a refresh token, you can use it to obtain a new ID token without requiring user interaction.
3. **Exchange again**: Send the new IdP token to the token exchange endpoint to obtain a fresh ESS Access Token.

### Audit Trail

All token exchange events are auditable. Each exchange operation generates an audit event that includes:

* The external Identity Provider issuer
* The external subject identifier
* The mapped ESS agent identifier
* The mapped ESS client identifier
* The token scope (if configured)

## Configuration

As part of the [installation process](/ess/3.0/installation.md), Inrupt provides base Kustomize manifests that require deployment-specific configuration inputs.

The following configuration options are available for the token exchange endpoint.

### Required Configuration

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_ISSUER**

The issuer URL for ESS Access Tokens. Used as the **`iss`** claim in issued tokens. Typically set to the Platform Management service URL.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_ISSUER=https://platform.{ESS_DOMAIN}
```

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_SIGNING\_KEY\_LOCATION**

Path to the private key in JWK format used for signing ESS Access Tokens. The key must be an EC P-256 key (ES256 algorithm).

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_SIGNING_KEY_LOCATION=/opt/platform/platform-signing-key.json
```

{% hint style="warning" %}
**Warning**

The signing key is used to sign ESS Access Tokens. Safeguard your signing key and store it securely. Do not commit signing keys to version control.
{% endhint %}

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_AUDIENCES**

A comma-separated list of ESS service URLs that can accept the issued tokens. Used as the **`aud`** claim when no specific audience is requested.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_AUDIENCES=https://storage.{ESS_DOMAIN},https://provision.{ESS_DOMAIN},https://vc.{ESS_DOMAIN},https://notification.{ESS_DOMAIN},https://mcp.{ESS_DOMAIN},https://authorization.{ESS_DOMAIN}
```

### Trusted Identity Provider Configuration

Each trusted Identity Provider is configured with a unique name and its own issuer and JWKS URL. Only tokens from configured IdPs are accepted for exchange. Configure an entry here for every issuer you want ESS to integrate with, including the [Solid OIDC Broker](/ess/3.0/services/advanced-configuration/service-oidc.md) if it is deployed.

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_IDPS\_{NAME}\_ISSUER**

The issuer URL for the trusted Identity Provider. Must match the **`iss`** claim in the IdP's tokens.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_KEYCLOAK_ISSUER=https://idp.example.com/realms/ess
```

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_IDPS\_{NAME}\_JWKS\_URL**

The JWKS URL for fetching the Identity Provider's public keys, used to validate incoming tokens.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_KEYCLOAK_JWKS_URL=https://idp.example.com/realms/ess/protocol/openid-connect/certs
```

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_IDPS\_{NAME}\_EXPECTED\_AUDIENCE**

Required. The expected **`aud`** claim in tokens from this IdP. Tokens that do not contain a matching audience claim are rejected.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_KEYCLOAK_EXPECTED_AUDIENCE=https://platform.example.com
```

{% hint style="info" %}
**Multiple IdPs**

To configure multiple Identity Providers, use a different name for each. For example:

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_OKTA_ISSUER=https://dev-123456.okta.com/oauth2/default
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_OKTA_JWKS_URL=https://dev-123456.okta.com/oauth2/default/v1/keys

INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_AZUREAD_ISSUER=https://login.microsoftonline.com/{tenant}/v2.0
INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_AZUREAD_JWKS_URL=https://login.microsoftonline.com/{tenant}/discovery/v2.0/keys
```

New IdPs can be added post-deployment without system redeployment.
{% endhint %}

{% hint style="warning" %}
**Important**

Only add trusted Identity Providers. Each IdP should be verified and vetted before being configured.
{% endhint %}

### Optional Configuration

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_TTL**

Default: **`300`** (5 minutes)

Time-to-live in seconds for issued ESS Access Tokens.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_TTL=300
```

#### **INRUPT\_PLATFORM\_TOKEN\_EXCHANGE\_SCOPE**

Optional scope claim to include in issued ESS Access Tokens. If not set, the scope claim is omitted.

```bash
INRUPT_PLATFORM_TOKEN_EXCHANGE_SCOPE=mcp:access-request:write mcp:access-request:read mcp:access-grant:read mcp:storage:read
```

#### **INRUPT\_JWT\_ISSUER\_ALLOW\_LIST**

A comma-separated list of issuer URLs accepted by the Solid JWT authentication layer. This setting applies only to deployments that support Solid JWT authentication.

```bash
INRUPT_JWT_ISSUER_ALLOW_LIST=https://idp.example.com/realms/ess
```

{% hint style="info" %}
**Relationship to trusted Identity Provider configuration**

This setting is separate from the [trusted Identity Provider configuration](#trusted-identity-provider-configuration) above, and the two act at different points in a request:

* `INRUPT_PLATFORM_TOKEN_EXCHANGE_IDPS_{NAME}_*` governs which issuers the Platform Management service will exchange tokens for. Configure it for every issuer you integrate with, including a native IdP.
* `INRUPT_JWT_ISSUER_ALLOW_LIST` applies earlier, in the Solid JWT authentication layer carried over from releases before ESS 3.0.0.

Validating a Solid JWT requires ESS to retrieve the presenting agent's WebID and confirm it lists the token's issuer as trusted. When `INRUPT_JWT_ISSUER_ALLOW_LIST` is set, tokens from issuers outside the list are rejected before that retrieval happens. Setting it is recommended wherever Solid JWT authentication is in use: it limits the work an unauthenticated client can force ESS to perform by presenting fabricated tokens, mitigating denial-of-service attempts against the Solid JWT authentication path.
{% endhint %}

### Kafka Configuration

{% hint style="info" %}
**Tip**

See also [ESS' Kafka Configuration](/ess/3.0/services/appendix/appendix-kafka-configuration.md)
{% endhint %}

#### **KAFKA\_BOOTSTRAP\_SERVERS**

Default: **`localhost:9092`**

Comma-delimited list of Kafka broker servers for audit event publishing.

## Additional Information

* [OAuth 2.0 Token Exchange (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693)
* [JWT Profile for OAuth 2.0 Access Tokens (RFC 9068)](https://datatracker.ietf.org/doc/html/rfc9068)
* [OpenID Connect](https://openid.net/connect/)
