Skip to main content

Embedded Login

Introduction​

Using Medplum as IDP describes the OAuth2 Authorization Code flow using a full browser redirect: your user leaves your application, authenticates on a Medplum-hosted page, and is redirected back with a code.

Embedded login is the same underlying OAuth2 Authorization Code flow with PKCE — it just skips the redirect. The email/password form is rendered directly inside your own application, using the <SignInForm> React component or the startLogin() SDK method directly. Your user never sees another domain.

Choose embedded login when you want full control over your login page's branding and don't want a visible navigation away from your app. Choose the redirect flow instead if you'd rather the password never be typed into your own application's code at all — since Medplum's hosted page runs on its own domain, a vulnerability in your app can't expose credentials it never saw.

How it works​

Both variants use the same two-request shape: authenticate to get a one-time code, then redeem that code for tokens. The difference is only in how request 1 happens — as a direct API call instead of a browser navigation.

The password is only ever checked in the first request. The second request never sees it — it only carries the one-time code plus the PKCE code_verifier needed to redeem it.

Prerequisites​

Embedded login uses the same ClientApplication setup as the redirect flow. If you haven't created one yet, follow the Create a Client Application steps on that page first.

Request 1: authenticate with credentials​

Call POST /auth/login with the user's credentials:

FieldDescription
emailThe user's email address
passwordThe user's password
clientId(Optional) Your ClientApplication ID
codeChallenge / codeChallengeMethodThe PKCE challenge for this login attempt (see PKCE)

On success, the response contains a login ID and a one-time code:

{
"login": "1a2b3c4d-...",
"code": "..."
}

If the user has multi-factor authentication enrolled, or belongs to more than one project, the response looks different — see Multi-factor authentication and multiple projects below.

Request 2: redeem the code​

Call POST /oauth2/token with the code from request 1:

FieldValue
grant_typeFixed value: authorization_code
codeThe one-time code from request 1
client_idYour ClientApplication ID
code_verifierThe PKCE verifier that produced the codeChallenge sent in request 1

A successful response contains your tokens — see Tokens below.

PKCE​

Both requests are bound together by PKCE (Proof Key for Code Exchange), so that the code returned by request 1 can only be redeemed by whoever started the login attempt.

  1. Before request 1, generate a random code_verifier and keep it locally. Hash it with SHA-256 to produce the codeChallenge sent to /auth/login.
  2. Medplum stores that codeChallenge alongside the Login it creates. It never sees the raw verifier at this point.
  3. In request 2, send the original, un-hashed code_verifier in the token request body.
  4. Medplum hashes the verifier you just sent and checks it matches the codeChallenge stored in step 2. A mismatch is rejected.

If you use the Medplum SDK (startLogin() or <SignInForm>), this is handled for you automatically — the verifier is generated, stored, and replayed without any code on your part.

PKCE is required by default

A ClientApplication can opt out of requiring PKCE by setting pkceOptional: true, in which case a client secret can be used instead. This isn't recommended for browser-based applications, since a secret embedded in client-side code isn't actually secret.

Multi-factor authentication and multiple projects​

Request 1's response isn't always { login, code }. A few other cases are possible before a code is issued:

Response fieldMeaningWhat to do next
mfaRequiredThe user has MFA enrolled and must provide a codePOST /auth/mfa/verify
mfaEnrollRequiredThe project requires MFA and the user hasn't enrolled yet. This response already includes a TOTP enrollment URI and QR code.POST /auth/mfa/login-enroll, submitting the code generated by the authenticator app the user just set up from that QR
membershipsThe user belongs to more than one projectPOST /auth/profile, naming which membership to use

Each of these follow-up calls returns the same shape as request 1 — either another prompt, or the { login, code } you need to move on to request 2.

See Multi-Factor Authentication for full details on the MFA enrollment and verification flow.

Tokens​

A successful token exchange returns:

TokenContents
access_tokenA JWT used as a Bearer token on every subsequent API request
id_tokenOpenID Connect identity claims for the logged-in user (fhirUser, auth_time, and email if the email scope was requested)
refresh_tokenPresent only if requested — see below

A refresh_token is included only when one of the following is true: the request included the offline_access scope, or your ClientApplication's registered grant types include refresh_token. For self-hosted, logins to a super admin project never receive a refresh token regardless of these settings.

Using the SDK​

The <SignInForm> React component handles both requests, PKCE, and the multi-factor/multiple-project branches automatically:

<SignInForm onSuccess={() => navigate('/')?.catch(console.error)}>
<Logo size={32} />
<h1>Sign in to Foo Medical</h1>
</SignInForm>

If you're not using React, call startLogin() directly with the MedplumClient to perform request 1, handle the response cases described above, then call processCode() with the returned code to complete request 2 and receive your tokens.

See Also​