SMART Permission Tickets
0.1.0 - ci-build
SMART Permission Tickets, published by . This guide is not an authorized publication; it is the continuous build for version 0.1.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/jmandel/smart-permission-tickets-wip/ and changes regularly. See the Directory of published versions
| Status: Draft for discussion | Author: Josh Mandel | Date: April 30, 2026 |
An app should be able to generate a Permission Ticket that a network or Data Holder can trust.
The app does not get this trust just by saying "trust me." It needs three things:
This proposal shows how that works for patient self-access. The patient uses an app, completes an IAL2 identity proofing flow, and the app signs a Permission Ticket authorizing itself to request the patient's data across a network.
For a Data Holder, the app-issued ticket answers four practical questions:
| Question | Where the answer appears |
|---|---|
| Who signed this authorization? | iss and the ticket signature |
| Which Data Holders or network is it for? | aud and aud_type |
| Which patient is it about? | embedded ID token in subject_identity_evidence |
| Which app may redeem it? | authenticated presenter must equal iss |
For app-issued patient self-access, the app is both the ticket issuer and the presenter. That means presenter_binding can be omitted: the Data Holder requires the authenticated presenting client to be the same entity as iss.
In the main example:
The app is not asking Hospital A to trust an arbitrary self-declared patient. It is asking Hospital A to evaluate a signed Permission Ticket from a recognized app issuer, plus a signed ID token from an identity provider.
Dorothy opens Health Wallet App and asks it to gather her records from the Community Health Network.
The app knows it is allowed to issue patient self-access tickets in that network. It has an entity identifier and signing keys that Data Holders can verify through the network's trust framework or local configuration.
app entity: https://wallet.example.org
ticket signing keys: published for https://wallet.example.org
network audience: https://community-network.example.org
The app sends Dorothy through an IAL2 identity proofing flow.
The identity provider returns an ID token to the app. The important point is that the ID token's audience identifies the app as the relying party:
id_token.iss = https://idp.example.org
id_token.aud = https://wallet.example.org
id_token.acr = IAL2-equivalent assurance value
The ID token is not meant to be an access token for Hospital A. It is evidence that Dorothy proved her identity to the app.
The app puts the ID token into subject_identity_evidence:
{
"source": "embedded",
"token_type": "id_token",
"jwt": "eyJhbGciOi...ID_TOKEN_FOR_WALLET_APP..."
}
The Data Holder verifies this ID token independently and uses its identity claims for patient resolution.
The app signs a Permission Ticket. The verified ID token is the patient-resolution input, so the ticket does not repeat patient demographics in subject.patient.
{
"iss": "https://wallet.example.org",
"aud": "https://community-network.example.org",
"aud_type": "trust_framework",
"exp": 1777584000,
"jti": "dorothy-wallet-001",
"ticket_type": "https://smarthealthit.org/permission-ticket-type/patient-self-access-v1",
"subject_identity_evidence": {
"source": "embedded",
"token_type": "id_token",
"jwt": "eyJhbGciOi...ID_TOKEN_FOR_WALLET_APP..."
},
"access": {
"permissions": [
{
"kind": "data",
"resource_type": "Observation",
"interactions": ["read", "search"]
},
{
"kind": "data",
"resource_type": "MedicationRequest",
"interactions": ["read", "search"]
}
],
"data_period": {
"start": "2021-01-01",
"end": "2026-01-01"
}
}
}
There is no requester because this is self-access. The person identified by subject_identity_evidence is the patient and requester.
There is no presenter_binding because the app is issuing the ticket for its own presentation. In this profile, absence of presenter_binding has a strict meaning: the client presenting the ticket must be the same entity as iss.
The app calls Hospital A's token endpoint using the normal Permission Ticket token exchange:
POST /token HTTP/1.1
Host: fhir.hospital-a.example.org
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:token-exchange
&subject_token=eyJhbGciOi...PERMISSION_TICKET...
&subject_token_type=https://smarthealthit.org/token-type/permission-ticket
&scope=patient/Observation.rs patient/MedicationRequest.rs
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion=eyJhbGciOi...CLIENT_ASSERTION_FROM_WALLET_APP...
The Permission Ticket is the authorization grant. The client_assertion authenticates the app that is presenting it.
Hospital A can evaluate the request without sending Dorothy through another login flow.
It checks:
client_assertion authenticates as https://wallet.example.org.https://wallet.example.org.aud covers Hospital A, either directly or through https://community-network.example.org.presenter_binding is absent, the authenticated presenter must equal the ticket issuer.https://wallet.example.org.access block.If those checks pass, Hospital A issues a scoped access token to the app.
The embedded ID token must have been issued to the ticket issuer as the relying party:
id_token.aud = permission_ticket.iss
That rule is what links the identity proofing event to the entity that signed the ticket.
If the ID token audience were an opaque value such as abc123, Hospital A would usually have no way to know whether abc123 is really the same entity as https://wallet.example.org. A profile can allow a different audience identifier only when Hospital A can verify the mapping through public trust-framework metadata or local configuration.
If the CMS Trusted App Library publishes stable identifiers for participating apps, that identifier becomes the shared anchor for this flow.
For example, the CMS Trusted App Library might publish:
app identifier: https://wallet.example.org
app name: Health Wallet App
allowed ticket types: patient-self-access-v1
signing keys: https://wallet.example.org/.well-known/jwks.json
status: active
Then the app-issued ticket uses that same identifier consistently:
client_assertion.iss = https://wallet.example.org
client_assertion.sub = https://wallet.example.org
permission_ticket.iss = https://wallet.example.org
embedded_id_token.aud = https://wallet.example.org
This makes the Data Holder's job more mechanical:
https://wallet.example.org in the CMS Trusted App Library.With this model, the Data Holder does not need a private mapping from an opaque identity-provider client ID to the app. The registry-published app identifier is the value that appears in the app authentication, the ticket issuer claim, and the ID token audience.
App-issued tickets make the app a real trust participant. Data Holders need a way to discover or configure the app's issuer identity and ticket-signing keys.
The embedded ID token may contain sensitive identity information. Implementations should treat Permission Tickets containing subject_identity_evidence as identity-bearing artifacts for logging, storage, and retention purposes.