An important part of building a research application is the OAuth 2.0 workflow outlined by SMART on FHIR. To demonstrate how to use the OAuth workflow, let's walk through the process and manually perform the steps that the research application needs to implement. For the demonstration, we'll use the FHIR server with the SMART Reference Server provided by the S4S Docker reference stack which is available at http://portal.demo.syncfor.science/, but you can follow these same steps using the sandbox environments from Cerner, Epic, and others (see http://dev.smarthealthit.org/ for details). The steps are:
Note: You can also run the walkthrough with a local instance of the SMART Reference Server running on Docker - just replace https://portal.demo.syncfor.science/
with http://localhost:9000/
throughout the examples.
A research application must register itself once with the SMART Reference Server before it can access any data. This registration workflow is implemented as described in RFC7591. Client registration is performed by sending a POST request to the registration endpoint at https://portal.demo.syncfor.science/oauth/register. The body of the request is a JSON payload with the application's name, an array of redirect URIs, and the desired scopes. Because we'll be building our example client right inside of this Jupyter Notebook, we won't be hosting it on a regular web server, so the redirect URI that we'll use is really just a placeholder. We can register the fake endpoint https://not-a-real-site/authorized as our placeholder and copy the code from the browser into the notebook at the end of the participant's authorization process.
In [1]:
import requests
from pprint import pprint
redirect_uri = 'https://not-a-real-site/authorized'
data = {
'client_name': 'Fake Research Application',
'redirect_uris': [redirect_uri],
'scope': 'launch/patient patient/*.read offline_access'
}
response = requests.post('https://portal.demo.syncfor.science/oauth/register', json=data)
response_data = response.json()
print(f'Response status code: {response.status_code}')
pprint(response_data)
client_id = response_data['client_id']
client_secret = response_data['client_secret']
The workflow is lauched by the participant by accessing the authorize endpoint exposed by the SMART Reference Server, which can be determined by looking at the server's conformance statement. For the S4S SMART Reference Server, this endpoint is https://portal.demo.syncfor.science/oauth/authorize.
In [2]:
from urllib.parse import urlencode
params = {
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri,
'scope': 'launch/patient patient/*.read offline_access',
'state': 'my-obscured-state',
'aud': 'https://portal.demo.syncfor.science/api/fhir'
}
print(f'https://portal.demo.syncfor.science/oauth/authorize?{urlencode(params)}')
By following that link, you may be asked to login with the default credentials (or you may already be logged in), and then asked to complete the authorization process. At the end of the process, you'll be redirected to a URL like https://not-a-real-site/authorized?code=5tQu3bV8XDTYgf8t3VOjzsbVZ5Fuqn&state=my-obscured-state, containing the authorization code. Your browser will display an error since there is no real web server here, but that's okay since you are manually playing the role of the web browser by extracting the code
URL parameter and pasting it back into this notebook. Copy this code into the snippet below, but act fast since this code expires in 100 seconds!
In [3]:
code = '5tQu3bV8XDTYgf8t3VOjzsbVZ5Fuqn' # replace with your code
With code in hand, we can now get an access token from the SMART Reference Server. This would be completed by your application without any input from the user. Note that since this is a confidential client, we need to use basic authentication when interacting with the SMART Reference Server, where the username and password are the client ID and client secret, respectively.
In [4]:
auth = (client_id, client_secret) # for basic authentication
data = {
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': redirect_uri
}
response = requests.post('https://portal.demo.syncfor.science/oauth/token', auth=auth, data=data)
response_data = response.json()
print(f'Response status code: {response.status_code}')
pprint(response_data)
access_token = response_data['access_token']
refresh_token = response_data['refresh_token']
patient_id = response_data['patient']
Note: If you receive a 401 response, you may have taken more than 100 seconds at which time the authorization code expires.
You can see that the server gave us the patient ID whose data we now have access to, and the access token we can use to get the data.
Now that we have a client ID, client secret, and access token, we can use these values to get some FHIR data, which our application can store. The access token is simply used as a bearer token in the header of the request to the SMART Reference Server. In the reference stack implementation, the SMART Reference Server then proxies the authenticated request to the HAPI-FHIR server. Normally, the request will fail if you try to access the patient data without the access token:
In [5]:
response = requests.get(f'https://portal.demo.syncfor.science/api/fhir/Patient/{patient_id}') # oops, no header
print(f'Response status code: {response.status_code}')
Now let's try the same request with the access token:
In [6]:
headers = {
'Authorization': f'Bearer {access_token}'
}
response = requests.get(f'https://portal.demo.syncfor.science/api/fhir/Patient/{patient_id}', headers=headers)
print(f'Response status code: {response.status_code}')
pprint(response.json())
Success!
Note: If you get a 500 server error, you may need to load the patient data into the FHIR server. This can be done by running docker-compose run tasks load-sample-data-stu2
. See the GitHub repository for more details.
If the access token has expired, the refresh token can be used to generate a new one with a POST request to the token endpoint at https://portal.demo.syncfor.science/oauth/token (using basic authentication):
In [7]:
auth = (client_id, client_secret)
data = {
'grant_type': 'refresh_token',
'refresh_token': refresh_token
}
response = requests.post('https://portal.demo.syncfor.science/oauth/token', auth=auth, data=data)
pprint(response.json())
Let's experiment with some additional examples showing some debug features available with the SMART Reference Server. For these examples, the SMART Reference Server is accessible at https://portal.demo.syncfor.science/. However, if you're running the S4S Docker reference stack locally with Docker, you can access your local SMART Reference Server at http://localhost:9000/.
Once a client has been registered, let's try the token debug endpoints that are included with the SMART Reference Server (for development purposes only) - real servers will not have endpoints available to generate or introspect tokens like this.
We can create an access token for a registered client with a POST request to https://portal.demo.syncfor.science/oauth/debug/token. The request should include the client ID obtained from registration, the username of a user of the SMART Reference Server, and the patient ID which is associated with the user. The S4S SMART Reference Server ships with a user named daniel-adams
which has access to a patient with ID smart-1288992
, so we'll use these.
In [8]:
username = 'daniel-adams'
patient_id = 'smart-1288992'
data = {
'client_id': client_id,
'username': username,
'patient_id': patient_id
}
response = requests.post('https://portal.demo.syncfor.science/oauth/debug/token', json=data)
response_data = response.json()
print(f'Response status code: {response.status_code}')
pprint(response_data)
access_token = response_data['access_token']
refresh_token = response_data['refresh_token']
Now that we have a token granting us access to a user, let's inspect it using a GET request to https://portal.demo.syncfor.science/oauth/debug/introspect:
In [9]:
params = {'token': access_token} # can be access or refresh token
response = requests.get('https://portal.demo.syncfor.science/oauth/debug/introspect', params=params)
print(f'Response status code: {response.status_code}')
pprint(response.json())
Here we can see information associated with this token. By default when creating a token with the reference stack's debug endpoint, the access token is valid for 1 hour (access_expires
), but the approval is valid for 1 year (approval_expires
). This means that after 1 hour, attempts to use the access_token
when fetching data will fail; however, the approval is still valid for 1 year, so the refresh_token
may be used to generate a new access token within this time frame. A real research application will have no control over these time periods - the SMART Reference Server decides how for long the access token should be valid, and the approval expiration time is generally specified by the participant during the authorization process.
For debugging purposes, these parameters can be specified in the request to the debug token endpoint:
In [10]:
from time import time
data = {
'client_id': client_id,
'username': username,
'patient_id': patient_id,
'access_lifetime': 3*60*60, # duration in seconds
'approval_expires': time() + 180*24*60*60 # UNIX timestamp
}
token_response = requests.post('https://portal.demo.syncfor.science/oauth/debug/token', json=data)
access_token = token_response.json()['access_token']
introspect_response = requests.get('https://portal.demo.syncfor.science/oauth/debug/introspect',
params={'token': access_token})
pprint(introspect_response.json())
The access_expires
and approval_expires
reflect the requested durations of 3 hours and 180 days, respectively.