## Overview

Medplum supports the [OAuth 2.0 Token Exchange](https://datatracker.ietf.org/doc/html/rfc8693) proposed standard. Token exchange provides a mechanism exchange an external access token for a Medplum access token without redirecting the user. This is useful when your application has already authenticated the user with an external identity provider, such as [Auth0](https://auth0.com/), and would access the Medplum API services without requiring the user to log in again.

## Set up your ClientApplication

In order to authenticate the user, Medplum's authentication server will call the `/userinfo` endpoint of the external identity provider and check for a valid response. Therefore, it is important that the corresponding [`ClientApplication`](https://app.medplum.com/ClientApplication) in your Medplum project has an identity provider set up with a user info URL specified.

To set up an identity provider for your [`ClientApplication`](https://app.medplum.com/ClientApplication):

1. Log in to your Medplum project.
2. Navigate to the [ClientApplications page](https://app.medplum.com/ClientApplication).
3. Click on the [`ClientApplication`](https://app.medplum.com/ClientApplication) that you would like to set up with an identity provider.
4. Click the "Edit" tab.
5. Scroll down to the "Identity Provider" section.
6. Enter the `/userinfo` URL for your external identity provider in the "User Info URL" textbox.
7. Click "Save" to save your changes.

## Scopes

By default, Medplum will use the user's email address to identify their Medplum user, so you must make sure that you have requested your external access token with the `email` scope.

If you would like to use the `sub` (subject) identifier assigned by the external authentication provider, you must check "Use Subject" in your "Identity Provider" settings.

## Exchanging tokens

Once you have set up the identity provider for your [`ClientApplication`](https://app.medplum.com/ClientApplication), you can use the `/oauth2/token` endpoint to exchange the external access token for a Medplum access token.

The client makes a token exchange request to the token endpoint with an extension grant type using the HTTP POST method. The following parameters are included in the HTTP request entity-body using the application/x-www-form-urlencoded format with a character encoding of UTF-8.

- **grant_type**: The constant value of "urn:ietf:params:oauth:grant-type:token-exchange".
- **client_id**: The selector for the external auth provider making the exchange request. This is either the ID of the [`ClientApplication`](https://app.medplum.com/ClientApplication) in your Medplum project, or the `clientId` of a server-level [`externalAuthProviders`](/content/docs/auth/token-exchange#server-scoped-users-and-the-membership_id-parameter-self-hosted-only/index.html) entry (self-hosted only).
- **subject_token**: The access token that was generated by the external identity provider.
- **subject_token_type**: The subject token type as defined in Section 3. Only "urn:ietf:params:oauth:token-type:access_token" is currently supported.
- **membership_id** (optional): The [`ProjectMembership`](/content/docs/api/fhir/medplum/projectmembership/index.html) that the resulting Medplum token should be scoped to. Use it to disambiguate when the resolved user has more than one candidate membership. The set of candidates depends on the `client_id`: a `ClientApplication` constrains the candidates to its own project, while a server-level provider considers the user's memberships across all projects — see [Server-scoped users and the `membership_id` parameter](/content/docs/auth/token-exchange#server-scoped-users-and-the-membership_id-parameter-self-hosted-only/index.html) below.

### Example

- TypeScript
- cURL

```ts
// Create MedplumClient

const medplum = new MedplumClient({

baseUrl: MEDPLUM_BASE_URL,

clientId: MEDPLUM_CLIENT_ID,

});

// Exchange external token

await medplum.exchangeExternalAccessToken(EXTERNAL_ACCESS_TOKEN);
```

```bash
curl -X POST 'https://api.medplum.com/oauth2/token' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \

--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \

--data-urlencode "client_id=<Your ClientApplication ID>" \

--data-urlencode "subject_token=<External Access Token>"
```

The response will include a Medplum access token, which you can use to access the Medplum API. The Medplum SDK also provides the [`exchangeExternalAccessToken()`](/content/docs/sdk/core.medplumclient.exchangeexternalaccesstoken) helper method.

## Server-scoped users and the `membership_id` parameter (self hosted only)

The setup above configures the external identity provider on a single [`ClientApplication`](https://app.medplum.com/ClientApplication). Because a `ClientApplication` lives inside one [`Project`](/content/docs/api/fhir/medplum/project/index.html), the exchange is implicitly scoped to that project: Medplum only considers the user's [`ProjectMembership`](/content/docs/api/fhir/medplum/projectmembership/index.html) records **within the client's project**. For most [project-scoped users](/content/docs/user-management/project-vs-server-scoped-users/index.html) that resolves to a single membership and the token "just works".

In two situations there is more than one candidate membership, and you need `membership_id` to pick deterministically:

- **Multiple memberships in the same project.** If the resolved user has more than one membership in the client's project, the exchange would otherwise fall back to the _first_ one. Pass `membership_id` to choose. This works with a plain `ClientApplication` — no server-level config required.
- **Reaching a different project (server-scoped users).** [Server-scoped users](/content/docs/user-management/project-vs-server-scoped-users#server-scoped-users/index.html) (typically developers and administrators) can have a membership in _many_ projects. A `ClientApplication` cannot reach across projects — it only ever resolves into its own project — so to target an arbitrary project you must use a **server-level external auth provider** (below), where `membership_id` both selects the membership and derives the project.

### Configure a server-level external auth provider

To support token exchange for server-scoped users, configure the identity provider at the server level via [`externalAuthProviders`](/content/docs/self-hosting/server-config/index.html) instead of (or in addition to) a per-`ClientApplication` identity provider. Unlike a `ClientApplication`, a server-level provider is not tied to any single project, so the exchange can resolve into whichever project you target.

Self-hosted only

`externalAuthProviders` is part of the [server config](/content/docs/self-hosting/server-config/index.html) and requires super admin access, so it is only available on self-hosted deployments. On Medplum's hosted service, token exchange is scoped to a `ClientApplication`'s project. If you need cross-project token exchange on hosted Medplum, contact [Medplum support](/content/contact/index.html).

```json
{

"externalAuthProviders": [\
\
    {\
\
      "issuer": "https://your-idp.example.com",\
\
      "clientId": "your-token-exchange-selector",\
\
      "userInfoUrl": "https://your-idp.example.com/oauth2/userinfo"\
\
    }\
\
  ]

}
```

The `clientId` you assign here is the value you pass as `client_id` in the exchange request. (If you omit the top-level `clientId`, the server falls back to `identityProvider.clientId`.) Restart the server after updating the config.

### Why you must pass `membership_id`

When you call the token endpoint with a server-level provider, Medplum validates the external token and resolves the user by email or `sub`. If you **do not** pass `membership_id`, the server falls back to the user's _first_ membership — which is non-deterministic for a server-scoped user that belongs to several projects, and may not be the project you intended.

To pick the project explicitly, include `membership_id` in the request. The server reads that [`ProjectMembership`](/content/docs/api/fhir/medplum/projectmembership/index.html), derives the project from it, and issues a token scoped to that membership:

```bash
curl -X POST 'https://your-medplum-server.example.com/oauth2/token' \

--header 'Content-Type: application/x-www-form-urlencoded' \

--data-urlencode "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \

--data-urlencode "subject_token_type=urn:ietf:params:oauth:token-type:access_token" \

--data-urlencode "client_id=your-token-exchange-selector" \

--data-urlencode "subject_token=<External Access Token>" \

--data-urlencode "membership_id=<ProjectMembership id>"
```

With the SDK, pass it as the third argument to [`exchangeExternalAccessToken()`](/content/docs/sdk/core.medplumclient.exchangeexternalaccesstoken):

```ts
await medplum.exchangeExternalAccessToken(externalAccessToken, clientId, membershipId);
```

### You own the identity-to-membership mapping

The external IdP token only carries the external identity (email or `sub`). It has no knowledge of Medplum projects or `ProjectMembership` ids. That means **your application is responsible for knowing which membership to target** for a given user and context, and for passing the right `membership_id` on each exchange:

- **Keep track of memberships in your own system.** Persist the mapping from your external user (and the project/tenant context they're acting in) to the corresponding Medplum `ProjectMembership` id, so you can look it up at exchange time.
- **Discover memberships when you need to.** You can enumerate a user's memberships by searching [`ProjectMembership`](/content/docs/api/fhir/medplum/projectmembership/index.html) resources (for example, by `profile` or `user`) with a sufficiently privileged token, then store the ids you care about.
- **Validate the target.** An invalid or unreadable `membership_id` is rejected with an `invalid_request` error, so confirm the membership exists and belongs to the expected user/project before relying on it.

Example

The [server-side token exchange example](https://github.com/medplum/medplum/tree/main/examples/medplum-token-exchange-demo) demonstrates this end to end: it configures a server-level `externalAuthProviders` entry, obtains an external token, and exchanges it for a Medplum token while optionally targeting a specific `membership_id`.

## Legacy `/auth/exchange` endpoint

Before supporting the OAuth 2.0 Token Exchange standard, Medplum provided the `/auth/exchange` endpoint. This endpoint is deprecated, and developers are encouraged to adopt the standard.
