HSWT
(Have Stethoscope Will Travel)
(Have Stethoscope Will Travel)
So I've come a long way with Dart, and it's now my preferred language (mostly for familiarity, although I think there are some other advantages). I'm going to once again try and understand how SMART on FHIR works. Not by choice, really, but because it's become the default authorization sequence.
So I'm going to be continuously referencing the above diagram. This is officially described by HL7.
A) The EHR opens a new browser instance (or iframe) pointing to the app's registered launch Url (app_launch_url in diagram). This is the Url that the app listens for in order to load, and must be pre-registered with the EHR prior to anything taking place. When the EHR launches the Url, it also saves the current context (user, patient, etc.) somewhere that the Authorization server (which is often the same server) can access it. There are then two parameters that are included.
iss - short for issuer, Url of the base FHIR endpoint for the EHR (hint: you're going to query this url /metadata in a minute)
launch - an 'opaque token' (whatever that means), that is later used for identifying the correct context
EXAMPLE:
https://app/launch?iss=https://my_fhir_server/fhir&launch=xyz123
B) Your app then issues a GET request on iss/metadata. For a standalone app, this is where you start. There's no primary EHR call initially, and you just have to know the fhir base url (or query .well-known/smart-configuration.json - I have never used this, or seen it being used.)
EXAMPLE:
GET https://my_fhir_server/fhir/metadata
C) This will then return a ConformanceStatement (note that in stu3 and R4, while it's referred to as a "ConformanceStatement", the resourceType is actually a "CapabilityStatement" - in dstu2 the resource is still named "ConformanceStatement"). Here is an example of HAPI's Capability/Conformance Statement. The conformance statement contains a lot of information, but for our purposes we're only interested in 2 urls.
{
"resourceType": "CapabilityStatement",
"rest": [
{
"mode": "server",
"security": {
"extension": [
{
"url": "http://fhir-registry.smarthealthit.org/StructureDefinition/oauth-uris",
"extension": [
{
"url": "token",
"valueUri": "https://dbhifhir.aidbox.app/auth/token"
},
{
"url": "authorize",
"valueUri": "https://dbhifhir.aidbox.app/auth/authorize"
...
These are of course:
The authorize Url
The token Url
Now it starts to get a little tricky.
D) The app now has the authorize endpoint. It queries (hits? pings? technically I think it's a GET request?) and it has the option to contain a number of parameters:
response_type: required. This can apparently only be the value 'code'. This appears to be the main mechanism for performing authentication.
client_id: required. This is the ID of the client (in this case an app, NOT the id of the user of the EHR). This must be registered ahead of time. It's how the EHR knows what app is making a request.
redirect_uri: required. This is also pre-registered. This one I'm slightly unclear on. For a server-based app, it makes sense. You have an initial launch url, and then a redirect uri for after the authorization process. I'm going to make another diagram for stand-alone apps later. I was confused about how this would work for something like a mobile app. It apparently makes use of custom protocols (myapp:// and then a redirect such as myapp://callback).
launch: for the EHR workflow, the launch must match the launch parameter that was specified initially when the app was opened from the EHR (see the example in section A, launch=xyz123) for standalone launches, no launch parameter is passed
scope: required. lots of options here, I'm going to discuss them separately. (on this page).
state: required. another opaque value. Used for maintaining state between request and callback. The app should create a random sequence that is passed as part of this request, that the server then passes back with the redirect_uri and the app should use for authentication.
aud: required. URL of the EHR resource server (where you're going to be getting your data from). HL7 says this will be the iss in case of an EHR launch, although it seems to me that it's almost always going to be the iss, regardless of the launch context.
E) The EHR authorization server now directs to the redirect url passed in D. It will also return the code (which was requested as the mandatory response_type). It will also return a state (the same state that was passed by the app).
EXAMPLE
Location: https://app/after-auth?
code=123abc&
state=98wrghuwuogerg97
F) The app in turn takes this code, and POSTs it to the token Url. Remember the second endpoint from the conformance statement way back in C above? That's the token url. Parameters passed:
grant_type: required. fixed value, authorization_code
code: the code that the app just received from the server, pass it back
redirect_uri: same as the one the app just sent the server in D
client_id: only passed for public apps
For confidential apps, you're going to pass an Authorization header with your post request. Again, HL7 has a decent example.
The other header that you'll need to pass is:
Content-type: application/x-www-form-urlencoded
Thus, when put all together, and doing my best Postman impression, it looks something like this (although only Authorization or client_id should be present, not both):
POST /token
Host: ehr
Content-Type: application/x-www-form-urlencoded
Authorization: Basic bXktYXBwOm15LWFwcC1zZWNyZXQtMTIz
response_type=code&
client_id=app-client-id&
redirect_uri=https%3A%2F%2Fmy_redirect_uri&
launch=xyz123&scope=launch&
state=98wrghuwuogerg97%26&
aud=https%3A%2F%2Fehr%2Ffhir
G) Taking a break, this is exhausting
+-------+
|Primary|
| EHR |
+-------+
| (A) GET https://{app_launch_url}?
| launch=123&
| iss=https://{fhir base url}
|
|
|
|
|
|
|
\/
+-----+ +---------+
| |--(B)------------------------------------>| |
| App | GET https://{fhir base url}/metadata |EHR FHIR |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| |<-(C)-------------------------------------| Server |
| | [Conformance statement including | |
| | OAuth 2.0 endpoint URLs] | |
| | +---------+
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| | +---------+
| App |--(D)------------------------------------>| |
| | Redirect https://{ehr authorize_url}? |EHR Authz|
| | scope=launch& | Server |
| | state=abc& | |
| | aud={fhir base url}& | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| App | Authorize App (may include end-user |EHR Authz|
| | authentication & authorization) | Server |
| |<-(E)-------------------------------------| |
| | Redirect | |
| | https://{app redirect_uri}? | |
| | code=123&... | |
| | | |
| | | |
| | | |
| | | |
| | Exchange code for access token | |
| | If confidential client, include secret | |
| |--(F)------------------------------------>| |
| | POST https://{token url? | |
| | grant_type=authorization_code&code=123&… | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| | | |
| |Authenticate app (if confidential client) | |
| | Issue new (access) token with context | |
| |<-(G)-------------------------------------| |
| | [access token response] +---------+
| |
| |
| | +---------+
| | Access Patient Data | |
| |--(H)------------------------------------>|EHR FHIR |
| | GET https://{fhir base url}/Patient/123 | |
| | [pass access token with request] | Server |
| | | |
| | | |
| | | |
| | Return FHIR resource to app | |
| |<-(I)-------------------------------------| |
| | {"resourceType": "Patient", | |
| | "birthDate":...} | |
| | | |
+-----+ +---------+