Token Exchange | Medplum

Overview

Medplum supports the OAuth 2.0 Token Exchange 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, 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 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:

  1. Log in to your Medplum project.
  2. Navigate to the ClientApplications page.
  3. Click on the 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, 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.

Example

// Create MedplumClient

const medplum = new MedplumClient({

baseUrl: MEDPLUM_BASE_URL,

clientId: MEDPLUM_CLIENT_ID,

});

// Exchange external token

await medplum.exchangeExternalAccessToken(EXTERNAL_ACCESS_TOKEN);
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() 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. Because a ClientApplication lives inside one Project, the exchange is implicitly scoped to that project: Medplum only considers the user's ProjectMembership records within the client's project. For most project-scoped users 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:

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 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 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.

{

"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, derives the project from it, and issues a token scoped to that membership:

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():

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:

Example

The server-side token exchange example 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.