SMART on FHIR turns an electronic health record into an OAuth protected API, and every app, payer feed and analytics job that connects to it becomes a holder of tokens that can read clinical records. In the United States the certified patient and population API and the CMS payer APIs are built on HL7 FHIR with SMART App Launch for authorisation, so most health systems now run a SMART authorisation server whether they planned to or not. This guide covers what the generic OAuth guides do not: SMART launch flows and launch context, the SMART v2 scope grammar, Backend Services for system to system access, the client authentication profiles, and how to operate, monitor and review the app population that results. It is written for health system identity and integration teams, EHR administrators and the developers of apps that connect to them. For the wider clinical identity picture see the Healthcare Identity Security Guide.
Key takeaways
- SMART is OAuth with healthcare rules on top. SMART App Launch 2.2.0 (STU 2.2, FHIR R4) adds launch context, a scope grammar for FHIR resources and fixed security requirements such as mandatory PKCE with S256 and a required
audparameter. - Scopes decide the blast radius of every token. A
patient/*.crudsorsystem/*.rsgrant covers every resource type, including types added later, so wildcard and population scopes need an owner and a review. - Backend Services are non-human identities with population access. They authenticate with a signed JWT (RS384 or ES384), should receive access tokens of no more than 300 seconds and should not receive refresh tokens.
- Regulation sets minimum token behaviour. Certified APIs must issue three month refresh tokens to qualifying apps and revoke an app's access within one hour of the patient's request, so revocation and logging must work at that speed.
- The app register is the control. Knowing each registered client, its owner, client type, keys, redirect URIs and granted scopes is what lets you answer "who can read patient data through the API" with evidence.
How SMART on FHIR authorisation works
SMART App Launch is an HL7 implementation guide, currently published as version 2.2.0 (STU 2.2) on FHIR R4. The specification groups its capabilities into discovery, two authorisation patterns, client authentication, scopes, token introspection, user access brands and persisted app state. Discovery happens at [base]/.well-known/smart-configuration, which must list grant_types_supported, token_endpoint, capabilities and code_challenge_methods_supported, plus authorization_endpoint when either launch type is supported.
| Pattern | Who uses it | Grant | Typical scopes | Main risk |
|---|---|---|---|---|
| EHR launch | Clinician apps opened inside the EHR | authorization_code with a launch parameter | launch, user/ or patient/ scopes, openid fhirUser | Context injection, over broad user scopes |
| Standalone launch | Patient apps and clinician apps started outside the EHR | authorization_code | launch/patient, patient/ scopes, offline_access | Long lived refresh tokens on consumer devices |
| Backend Services | Payer feeds, population health, bulk export, integration engines | client_credentials with a JWT client assertion | system/ scopes | Population scale access from one private key |
The capabilities array tells you what the server will accept. Strings such as client-public, client-confidential-symmetric, client-confidential-asymmetric, permission-offline, permission-v1 and permission-v2 are worth reviewing: each one you advertise is an option an app can request. NHI Mgmt Group's view is that a server should advertise only the client types and permission models it has a policy for.
The SMART v2 scope grammar and how to constrain it
SMART v2 scopes take the form context/Resource.permissions. The context is patient/ (the patient in context), user/ (whatever the signed in user can see) or system/ (whatever the client system is authorised for). Permissions are letters from cruds (create, read, update, delete, search) in that order, so patient/Observation.rs grants read and search. Servers that advertise permission-v1 map the older suffixes: .read to .rs, .write to .cud and .* to .cruds.
Version 2 also allows granular scopes with a query string, for example patient/Observation.rs?category=http://terminology.hl7.org/CodeSystem/observation-category|laboratory, which limits a token to laboratory results. The specification lists search modifiers, chaining and _filter inside scopes as experimental, so treat them as unsupported unless your server documents them.
| Scope pattern | What it grants | Review question |
|---|---|---|
patient/*.cruds | Full read and write on every resource type for the patient in context, including types added in future | Does the app need write access at all, and which resource types does it really use? |
user/*.rs | Read of everything the user can see, which for a clinician may be the whole patient population | Can this be a patient/ scope with a launch context instead? |
system/*.rs | Read of all resources the client is authorised for, without a user | Is there a named data owner and a documented purpose for population access? |
patient/Observation.rs?category=...|laboratory | Read and search limited to one category | Does the server enforce the query constraint on every search path? |
offline_access | A refresh token that works when the user is not online | Is the client confidential or a native app that can protect the token? |
- Scopes are a ceiling, not a grant of rights: the specification is explicit that scopes delegate access but do not override underlying permissions, so a user scope still has to be filtered by the EHR's own access model.
- Clients must read the granted scope: the server can grant less than requested, including an empty scope string, and apps should not assume the request was honoured.
- Prefer POST for long scope lists: the guide warns that large scope strings can break URL and header limits, and servers advertise
authorize-postwhen they accept POST authorisation requests.
For general scope design see the OAuth Scope Design Guide.
Securing user facing launches
SMART App Launch fixes several choices that general OAuth leaves open. These are the settings to verify on the server and require of every app.
- PKCE: all SMART apps shall support PKCE, servers shall support
S256and shall not supportplain, and the server must check thecode_verifierat the token endpoint. Confirm thatcode_challenge_methods_supportedlists onlyS256. See the PKCE Implementation guide. - State: apps must send an unpredictable
statewith at least 122 bits of entropy, bind it to the user's session and validate it on return. - Audience: the
audparameter is required and names the FHIR server the app wants. The specification states its purpose as preventing a genuine bearer token leaking to a counterfeit resource server, and the resource server must check that the token's audience matches its own endpoint. - Redirect URIs: apps register fixed, fully specified redirect URIs and shall not forward values received at the redirect URI to arbitrary URLs. Pair this with exact string matching on the server, as RFC 9700 requires.
- Launch context: the
launchvalue from the EHR must be echoed back in the authorisation request. Treat launch context as server issued, never as something an app can set from a query string. - Token handling: access tokens should live no more than one hour, token responses carry
Cache-Control: no-store, bearer tokens must not sit in clear text cookies, and apps should not send their token to a different server referenced inside a FHIR resource. - Identity tokens: when
openid fhirUseris requested, theid_tokenmust be validated per OpenID Connect before thefhirUserclaim is trusted.
Browser based SMART apps carry the same token theft risk as any single page application. The OAuth for Browser-Based Applications and the BFF Pattern Guide covers keeping tokens server side, and the OAuth for Native and Mobile Apps Guide covers patient apps on phones.
Client types and refresh tokens
The specification says to register an app as confidential only if it can protect a secret, and that confidential apps should use asymmetric authentication where available. Refresh tokens must be bound to the same client_id, can only narrow scope at refresh time, and the app should discard the old refresh token when a new one is issued.
| Client profile | Authentication at token endpoint | Suitable for | Controls to require |
|---|---|---|---|
client-public | None, client_id only | Browser apps without a back end, apps that cannot keep a secret | PKCE, short access tokens, refresh token rotation or sender constraint per RFC 9700 |
client-confidential-symmetric | Client secret (client_secret_basic or client_secret_post) | Legacy server side apps | Secret stored in a secrets manager, rotation schedule, move to asymmetric |
client-confidential-asymmetric | private_key_jwt signed with RS384 or ES384 | Server side apps and all Backend Services | Hosted JWK Set, key rotation, replay checks on jti |
US certification adds fixed numbers. The ONC certification criterion for the standardised patient and population API, 45 CFR 170.315(g)(10), requires that confidential apps, and native apps that can secure a refresh token, receive a refresh token valid for no less than three months, and that an authorised application's access is revoked at the patient's direction within one hour of the request. A three month token on a patient's phone is a long lived credential, so revocation, refresh token rotation and theft detection carry more weight here than in most OAuth deployments. See the OAuth Refresh Token Rotation and Expiry Guide and How to detect use of a stolen OAuth refresh token.
Backend Services: the non-human identities in the EHR
Backend Services covers clients that run without a user: population health platforms, payer and provider data exchange, bulk export jobs and integration engines. The client must be confidential and use the client-confidential-asymmetric profile. It posts to the token endpoint with grant_type=client_credentials, a client_assertion_type of urn:ietf:params:oauth:client-assertion-type:jwt-bearer, a one time signed JWT and a required scope, over TLS 1.2 or later.
- Assertion rules: the JWT header carries
alg(RS384 or ES384), akidunique in the client's key set andtypofJWT. Claims setissandsubto the client ID,audto the token endpoint, a uniquejti, and anexpno more than five minutes in the future. - Server validation: the server rejects a reused
jtifor the same issuer within the assertion lifetime, accepts ajkuheader only if it matches the registered JWK Set URL, and requires exactly one key matchingkid. Any failure returnsinvalid_client. - Key registration: a JWK Set URL, reachable over TLS without authentication, is strongly preferred because the client can rotate keys by publishing a new key alongside the old one. Registering a static JWK Set is strongly discouraged because rotation then needs a manual update.
- Token lifetime:
expires_inshould not exceed 300 seconds and refresh tokens should not be issued. A Backend Services client asks for a new token when it needs one. - Scope: requests use
system/scopes such assystem/Observation.rs, and the granted scope in the response can differ from what was asked.
The private key behind a Backend Services client can read data for every patient in its scope, so treat it as you would a privileged service account. NHI Mgmt Group recommends keeping it in a hardware security module or cloud key service where the signing operation happens without exporting the key, rotating it at least annually and on any staff or supplier change, and recording an owner for every client. See the JWT Client Authentication (private_key_jwt) Guide and the Machine-to-Machine OAuth (Client Credentials) Guide.
Implementing SMART securely: a phased approach
- Inventory: export every registered SMART client with its client type, owner, redirect URIs, JWK Set URL or secret, granted scopes, refresh token policy and last use. Mark which clients are patient apps, clinician apps and Backend Services.
- Baseline the server: read your own
.well-known/smart-configuration. ConfirmS256only, remove client types and permission strings you do not intend to support, and set access token lifetimes (one hour or less for user apps, 300 seconds for Backend Services). - Set an app policy: define which scope patterns each app category may request, when wildcards are allowed, who approves
system/scopes and which apps may holdoffline_access. Write it down so reviewers apply one standard. - Pilot with a test patient: register apps against a non-production FHIR endpoint, test each launch type, and check that granular scopes are enforced on every search path you expose, including
_includeand_revinclude. - Migrate secrets to keys: move confidential clients from client secrets to
private_key_jwtwith hosted JWK Sets, keeping the old method enabled per client until the new one is proven, which is your rollback. - Prove revocation: test that a patient's revocation stops API access within one hour, including outstanding refresh tokens, and that a disabled Backend Services client fails on its next token request.
- Operate: run a quarterly review of scopes, owners and unused clients, and feed token endpoint and FHIR access logs to the SOC.
Decision guidance
- Patient app or Backend Service: if a person is present and the access is about that person, use a launch flow with
patient/scopes. Use Backend Services only for automated processing with an agreed population and purpose. - Granular scopes or a separate client: where the server enforces granular scopes reliably, use them. Where it does not, register separate clients per data purpose so each key and token can be revoked on its own.
- Symmetric or asymmetric: choose asymmetric for anything new. Keep symmetric secrets only for legacy confidential apps on a dated migration plan.
- Small practice or large health system: a small practice using a hosted EHR mainly controls app approval, scope approval and patient revocation through the vendor's administration console. A large system that runs its own authorisation server owns the full configuration, logging and key management described here.
Detection and measurement
Log token endpoint events (client ID, grant type, requested and granted scope, assertion jti, source IP, result) and FHIR API access (client ID, patient or system context, resource types, result counts). Alert on a Backend Services client requesting a new scope, a sudden rise in the number of distinct patients read by one client, refresh token use from a new network or device fingerprint, repeated invalid_client failures, and any token presented to an audience it was not issued for. Token introspection and revocation endpoints, when advertised, let the resource server check token state in real time; see the OAuth Token Introspection and Revocation Guide.
- Owner coverage: percentage of registered clients with a named owner and purpose (target 100 percent).
- Wildcard exposure: number of clients holding
*resource scopes, trending down. - Asymmetric adoption: percentage of confidential clients using
private_key_jwt. - Revocation time: measured time from patient revocation to failed API call, which must stay under one hour for certified APIs.
- Dormant clients: clients with no token issued in 90 days, reviewed and disabled.
Framework mapping
| Control area | NIST SP 800-53 Rev 5 | NIST CSF 2.0 | HIPAA Security Rule |
|---|---|---|---|
| Scope approval and least privilege | AC-3, AC-6 | PR.AA-05 | 164.312(a)(1) access control |
| Client registration and owners | AC-2, IA-9 | PR.AA-01 | 164.308(a)(4) information access management |
| Client authentication and keys | IA-5, SC-12 | PR.AA-03 | 164.312(d) person or entity authentication |
| TLS for all exchanges | SC-8 | PR.DS-02 | 164.312(e)(1) transmission security |
| Token and API logging | AU-2 | PR.PS-04, DE.CM-03 | 164.312(b) audit controls |
The relevant ATT&CK techniques are T1528 Steal Application Access Token and T1550.001 Application Access Token. See the HIPAA Security Rule Guide for the wider rule.
Common failure patterns
- Wildcard scopes by default: apps request
patient/*.crudsbecause it is easy, and approvals pass it through, so a read only app holds write access to every resource type. - User scopes where patient scopes would do: a clinician app with
user/*.rscan read every record the clinician can, turning one stolen token into population access. - Backend keys in files: the signing key for a population feed sits in a configuration file or container image instead of a key service, with no owner and no rotation date.
- Revocation that misses refresh tokens: the app is removed from the patient's portal view but its refresh token still works, breaking the one hour requirement.
- Granular scopes assumed but not enforced: the server accepts a category constrained scope but returns all categories on some search paths, which only testing finds.
- Accepting tokens without checking audience: a resource server that does not compare the token audience with its own endpoint lets tokens issued for one FHIR server work on another.
Practitioner checklist
- Read your
.well-known/smart-configurationand confirmS256only and only the client types and permission strings you support. - Keep a register of every SMART client with owner, purpose, client type, keys, redirect URIs, scopes and last use.
- Write and apply a scope approval policy covering wildcards,
user/scopes,system/scopes andoffline_access. - Require
private_key_jwtwith a hosted JWK Set for all Backend Services and new confidential apps. - Set access token lifetimes to one hour or less for apps and 300 seconds for Backend Services, with no refresh tokens for Backend Services.
- Enforce exact redirect URI matching and audience checks on every resource server.
- Test patient revocation end to end, including refresh tokens, against the one hour requirement.
- Send token endpoint and FHIR access logs to the SOC with alerts for scope changes and unusual patient counts per client.
- Review clients quarterly and disable those unused for 90 days.
Standards and references
- HL7: SMART App Launch Implementation Guide
- HL7: SMART App Launch, App Launch: Launch and Authorization
- HL7: SMART App Launch, Scopes and Launch Context
- HL7: SMART App Launch, Backend Services
- HL7: SMART App Launch, Client Authentication: Asymmetric
- HL7: SMART App Launch, Conformance
- ASTP/ONC: Standardized API for Patient and Population Services, 170.315(g)(10)
- IETF: RFC 9700 Best Current Practice for OAuth 2.0 Security
Related NHI Mgmt Group resources: Healthcare Identity Security Guide · HL7 and Interface Engine Security Guide · OAuth 2.0 Security Best Current Practice (RFC 9700) Guide · OAuth Token Theft Incident Playbook