Passer au contenu

Authentication

Authenticate to the EDFlow Integration API: OAuth 2.0 client credentials with private_key_jwt, client secret or mTLS, short-lived JWT access tokens, scopes, network binding, HMAC source keys for the signed ingest profile, clock skew, lockouts and signing at send time.

Cette page n'est pas encore offerte en français; elle est affichée en anglais.

There are two credential families:

  • API credentials for the REST API (/v1/*): OAuth 2.0 client credentials that return a 5-minute JWT access token, or (in limited cases) a static API key.
  • HMAC source keys for the signed ingest profile (/ehr/v1/*), for engines already built to it.

Methods

MethodHowRecommended forLive policy
private_key_jwtSign a JWT assertion with your private key; register a JWKS URL or public keysEMR vendors, Epic and Oracle system accounts, anyone who can sign JWTsAlways allowed. Recommended
client_secret_basic / client_secret_postClient id and secretEngines that can't sign JWTsOnly with an enforced IP allowlist or mTLS, and never for sensitive scopes
mTLS (tls_client_auth, self_signed_tls_client_auth)Client certificate on the mTLS host; the token is bound to itHospitals that mandate mutual TLSAlways allowed
Static API keyAuthorization: Bearer edf_{env}_sk_…Prototypes, simple reads, FHIR subscription rest-hooksOff by default; IP allowlist and ≤ 90-day expiry when allowed
HTTP Basic with an API keyclient_id:api_keyHL7-over-HTTP senders that only do BasicAs static keys, /v1/hl7v2 only
HMAC source keyPer-request signature over the raw bodyEngines built to /ehr/v1/*IP allowlist required; key versions expire within 365 days
Capture d'écran à venir : dev-console-create-credentialCette capture d'écran est prise automatiquement dans l'application, avec des données fictives.
WebCréer un identifiant

Get a token

POST /oauth/token with grant_type=client_credentials. Optional scope narrows the token.

private_key_jwt

The assertion: header alg RS384, ES384, RS256 or ES256 and a kid from your registered keys; iss and sub equal your client id; aud is exactly the token endpoint URL (https://api.edflow.space/oauth/token in live); exp at most 5 minutes ahead; a unique jti.

import { importPKCS8, SignJWT } from "jose";

const tokenUrl = `${process.env.EDFLOW_API_BASE_URL}/oauth/token`;
const key = await importPKCS8(process.env.EDFLOW_PRIVATE_KEY_PEM ?? "", "ES384");
const assertion = await new SignJWT({})
  .setProtectedHeader({ alg: "ES384", kid: process.env.EDFLOW_KEY_ID ?? "", typ: "JWT" })
  .setIssuer(process.env.EDFLOW_CLIENT_ID ?? "")
  .setSubject(process.env.EDFLOW_CLIENT_ID ?? "")
  .setAudience(tokenUrl)
  .setJti(crypto.randomUUID())
  .setIssuedAt()
  .setExpirationTime("4m")
  .sign(key);

const res = await fetch(tokenUrl, {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_assertion_type: "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
    client_assertion: assertion,
    scope: "requests:read requests:write",
  }),
});
const { access_token, expires_in } = await res.json();

The portal can generate a key pair in your browser and download the private key; only the public key is sent. Prefer a JWKS URL: you rotate by publishing a new kid.

Access tokens

JWTs (RFC 9068) signed by EDFlow, valid 60 to 300 s (default 300). They carry your organization, sites, scopes, environment, the credential used and your pinned API version. Every request re-checks the app, the credential and the site, so revocations and scope reductions apply immediately, not at expiry. Cache the token and refresh 30 s before it expires.

Discovery: /.well-known/oauth-authorization-server (RFC 8414), /oauth/jwks.json, and /v1/fhir/.well-known/smart-configuration for SMART Backend Services clients.

Scopes

ScopeGrantsSensitive
events:write, fhir:write, hl7v2:writeIngest
events:validateDry-run validation
events:readIngest results (metadata only)
encounters:readEncounter workflow state, lookup by visit number✓
careteam:readCurrent care team✓
requests:read, requests:writePatient requests feed; acknowledge and resolve✓
tasks:readPatient tasks✓
codes:readDepartment code alerts
documents:readVisit documents✓
webhooks:read, webhooks:writeWebhook management
sandbox:writeSandbox simulation and resetSandbox only

Sensitive scopes need private_key_jwt or mTLS, a four-eyes approval and the privacy office's PIA attestation in live, and the site's capability switch. A token obtained with a client secret never carries them.

Network binding

In customer test and live every app must have at least one of: private_key_jwt with registered keys, mTLS, or an IP allowlist. A leaked client secret is useless from any address outside the allowlist.

The signed ingest profile

Requests to /ehr/v1/* carry EDFlow-Source, EDFlow-Delivery-Id (new for each attempt), EDFlow-Timestamp and EDFlow-Signature:

signedPayload = "v2." + timestamp + "." + deliveryId + "." + METHOD + " " + path + "."
                + (siteCode or "-") + "." + (input or "-") + "."
                + lowercase_hex(SHA-256(raw body))
signature     = lowercase_hex(HMAC-SHA256(sourceKey, signedPayload))
header        = "EDFlow-Signature: v2=" + signature

The key is the 32 raw bytes of the 64-hex key revealed once in the admin console, not the ASCII of the hex. Sign the exact bytes you send. During a rotation you may send two signatures separated by a comma. Verify your implementation against the conformance test vector.

Sign and authenticate at send time

Engines retry from queues that can hold hours of messages. Compute the signature and fetch or refresh the token in the step that sends the request (a Mirth/OIE JavaScript Writer or destination pre-send script; a Rhapsody or Cloverleaf send-time script), never in the transformer, whose output queued retries replay unchanged.

ResponseWhat it meansDo this
401 token_expired (retryAction: reauthenticate)Token expired while queuedFetch a new token, resend the same request
401 timestamp_out_of_window, delivery_replayed (retryAction: resign)Signed too long ago, or delivery id reusedRe-sign with a new timestamp and delivery id; event ids stay the same
401 invalid_client, invalid_tokenWrong credentialFix it; don't loop

An exact replay of a signed delivery that was already processed returns the stored result, so a client that timed out learns what happened.

Clock skew

Timestamps more than 5 minutes from EDFlow's clock are refused. Keep engine clocks on NTP and compare with GET /ehr/v1/health at startup.

Lockouts

Ten failed authentications in 5 minutes from one (client id, IP) pair lock that pair for 15 minutes; 50 from one IP lock that IP. A lockout never blocks a valid credential, an allowlisted address or a registered mTLS certificate, so nobody can lock your integration out by guessing.

Capture d'écran à venir : dev-console-secret-revealCette capture d'écran est prise automatiquement dans l'application, avec des données fictives.
WebCopier votre secret

Dernière mise à jour le

Cette page vous a-t-elle été utile?

Sur cette page