OAuth Workflow Walkthrough

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:

  1. Register our application as a client
  2. Launch the workflow by asking the SMART Reference Server for authorization
  3. Collect the authorization code from the SMART Reference Server
  4. Exchange the code for an access token
  5. Use the access token to access patient data

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.

Registering the client

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']


Response status code: 201
{'client_id': 'f05b268b-78aa-4f51-bd89-a98052c18731',
 'client_name': 'Fake Research Application',
 'client_secret': '0e541908-c799-464f-9c96-fd1aa43cd3aa',
 'client_secret_expires_at': 0,
 'redirect_uris': ['https://not-a-real-site/authorized'],
 'scope': 'launch/patient patient/*.read offline_access'}

Launch the OAuth workflow

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)}')


https://portal.demo.syncfor.science/oauth/authorize?response_type=code&client_id=f05b268b-78aa-4f51-bd89-a98052c18731&redirect_uri=https%3A%2F%2Fnot-a-real-site%2Fauthorized&scope=launch%2Fpatient+patient%2F%2A.read+offline_access&state=my-obscured-state&aud=https%3A%2F%2Fportal.demo.syncfor.science%2Fapi%2Ffhir

Collect the authorization code

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

Exchange for access token

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']


Response status code: 200
{'access_token': 'VgxhidYLnQ75A0Qjuie1qijmesmr25',
 'expires_in': 3600,
 'patient': 'smart-1288992',
 'refresh_token': 'lETVw4xZIuBr6RZBw9vghTQJQyC90Z',
 'scope': 'launch/patient patient/*.read offline_access',
 'token_type': 'Bearer'}

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.

Using the tokens to retreive 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}')


Response status code: 401

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())


Response status code: 200
{'active': True,
 'address': [{'city': 'Tulsa',
              'country': 'USA',
              'line': ['1 Hill AveApt 14'],
              'postalCode': '74117',
              'state': 'OK',
              'use': 'home'}],
 'birthDate': '1925-12-23',
 'gender': 'male',
 'id': 'smart-1288992',
 'identifier': [{'system': 'http://hospital.smarthealthit.org',
                 'type': {'coding': [{'code': 'MR',
                                      'display': 'Medical record number',
                                      'system': 'http://hl7.org/fhir/v2/0203'}],
                          'text': 'Medical record number'},
                 'use': 'usual',
                 'value': '1288992'}],
 'meta': {'lastUpdated': '2018-02-21T15:01:44.340+00:00',
          'security': [{'code': 'patient',
                        'system': 'http://smarthealthit.org/security/categories'},
                       {'code': 'Patient/smart-1288992',
                        'system': 'http://smarthealthit.org/security/users'}],
          'versionId': '2'},
 'name': [{'family': ['Adams'], 'given': ['Daniel', 'X.'], 'use': 'official'}],
 'resourceType': 'Patient',
 'telecom': [{'system': 'email', 'value': 'daniel.adams@example.com'}],
 'text': {'div': '<div xmlns="http://www.w3.org/1999/xhtml">        <p>Daniel '
                 'Adams</p>      </div>',
          'status': 'generated'}}

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())


{'access_token': 'unZKFxECv0gXph2thIBajSpcH89gUs',
 'expires_in': 3600,
 'patient': 'smart-1288992',
 'refresh_token': 'UGoQoQ3WdZRWvoBKuCIo6jMMD9yIVK',
 'scope': 'launch/patient patient/*.read offline_access',
 'token_type': 'Bearer'}

SMART Reference Server Developer Resources

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/.

Token debug endpoints

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.

Create a token

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']


Response status code: 200
{'access_token': 'f74eb8f4-1656-45ae-89c9-91925fb454cc',
 'refresh_token': '58411c13-22f0-48ac-840a-fea1800c96a9'}

Inspect a 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())


Response status code: 200
{'access_expires': 'Fri, 13 Jul 2018 17:21:56 GMT',
 'access_token': 'f74eb8f4-1656-45ae-89c9-91925fb454cc',
 'active': True,
 'approval_expires': 'Sat, 13 Jul 2019 16:21:56 GMT',
 'client_id': 'f05b268b-78aa-4f51-bd89-a98052c18731',
 'refresh_token': '58411c13-22f0-48ac-840a-fea1800c96a9',
 'scope': 'launch/patient patient/*.read offline_access',
 'security_labels': [],
 'token_type': 'Bearer',
 'username': 'daniel-adams'}

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())


{'access_expires': 'Fri, 13 Jul 2018 19:21:59 GMT',
 'access_token': 'c376f069-f790-4611-9e78-a180b9965ecd',
 'active': True,
 'approval_expires': 'Wed, 09 Jan 2019 16:21:59 GMT',
 'client_id': 'f05b268b-78aa-4f51-bd89-a98052c18731',
 'refresh_token': 'e460ce1d-316e-4d1a-ba06-7975fc1b3fa4',
 'scope': 'launch/patient patient/*.read offline_access',
 'security_labels': [],
 'token_type': 'Bearer',
 'username': 'daniel-adams'}

The access_expires and approval_expires reflect the requested durations of 3 hours and 180 days, respectively.