Token Exchange
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:
- Log in to your Medplum project.
- Navigate to the ClientApplications page.
- Click on the
ClientApplicationthat you would like to set up with an identity provider. - Click the "Edit" tab.
- Scroll down to the "Identity Provider" section.
- Enter the
/userinfoURL for your external identity provider in the "User Info URL" textbox. - Click "Save" to save your changes.
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.
- 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
ClientApplicationin your Medplum project, or theclientIdof a server-levelexternalAuthProvidersentry (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
ProjectMembershipthat 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 theclient_id: aClientApplicationconstrains the candidates to its own project, while a server-level provider considers the user's memberships across all projects — see Server-scoped users and themembership_idparameter below.
- TypeScript
- cURL
// 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:
- 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_idto choose. This works with a plainClientApplication— no server-level config required. - Reaching a different project (server-scoped users). Server-scoped users (typically developers and administrators) can have a membership in many projects. A
ClientApplicationcannot 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), wheremembership_idboth 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 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.
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:
- 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
ProjectMembershipid, so you can look it up at exchange time. - Discover memberships when you need to. You can enumerate a user's memberships by searching
ProjectMembershipresources (for example, byprofileoruser) with a sufficiently privileged token, then store the ids you care about. - Validate the target. An invalid or unreadable
membership_idis rejected with aninvalid_requesterror, so confirm the membership exists and belongs to the expected user/project before relying on it.
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.