5

I'm trying to use the sandbox from https://fhir.epic.com/ for Backend Services.

I am following this tutorial : https://fhir.epic.com/Documentation?docId=oauth2&section=BackendOAuth2Guide :

  • I already register a new app,
  • created a JWT (using SSL keys)
  • tested the JWT on https://jwt.io/ : works fine!

But I cannot POST the JWT to the endpoint to obtain the access token. I should send a POST request to this URL: https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token.

I'm using python and this is my code so far:

import json
import requests
from datetime import datetime, timedelta, timezone
from requests.structures import CaseInsensitiveDict
from jwt import (
    JWT,
    jwk_from_dict,
    jwk_from_pem,
)
from jwt.utils import get_int_from_datetime


def main():
    instance = JWT()
    message = {
        # Client ID for non-production
        'iss': '990573e-13e3-143b-8b03-4fbb577b660',
        'sub': '990573e-13e3-143b-8b03-4fbb577b660',
        'aud': 'https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token',
        'jti': 'f9eaafba-2e49-11ea-8880-5ce0c5aee679',
        'iat': get_int_from_datetime(datetime.now(timezone.utc)),
        'exp': get_int_from_datetime(datetime.now(timezone.utc) + timedelta(hours=1)),
    }

    # Load a RSA key from a PEM file.
    with open('/home/user/ssl/privatekey.pem', 'rb') as fh:
        signing_key = jwk_from_pem(fh.read())

    compact_jws = instance.encode(message, signing_key, alg='RS384')
    print(compact_jws)

    headers = CaseInsensitiveDict()
    headers['Content-Type'] = 'application/x-www-form-urlencoded'

    data = {
      'grant_type': 'client_credentials',
      'client_assertion_type': 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
      'client_assertion': compact_jws
    }
    
    x = requests.post('https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token', headers=headers, data=data)
    print(x.text)

But I always get a 400 error:

{
  "error": "invalid_client",
  "error_description": null
}

Is the URL correct? How can I get an Access Token to play with the Sandbox?

cuzureau
  • 330
  • 2
  • 17

1 Answers1

4
'exp': get_int_from_datetime(datetime.now(timezone.utc) + timedelta(hours=1)),

At first glance, this appears to be your issue. Epic requires that exp be no more than 5 minutes in the future.

Couple of pieces of advice, beyond that:

  1. Use a library available from jwt.io
  2. Jwt.io also has a debugger where you can paste in your JWT to verify it is valid
Ashavan
  • 623
  • 3
  • 8
  • I already used a library and the debugger from jwt.io. The issue was indeed the time defined in `exp` ! Thanks – cuzureau May 17 '21 at 12:54
  • 1
    We also require a unique JTI on each request. If you are using a hard-coded JTI like you have in your code above, the first request will probably work, but subsequent ones with the same JTI will fail. You can just generate a new GUID each time and set that as the JTI. – Cooper May 17 '21 at 13:15
  • @Cooper indeed, I now generate a random ID every time I call my function – cuzureau May 20 '21 at 15:55
  • @ExceptionAl, I'm experiencing the same issue as the original poster. I validated my tokens and everything seems in order on my end (including the "exp" timestamp). Looking at my app, it says that it's in the "draft" state. Is there anything additional that needs to be done to have this work? – Chris J. Karr May 24 '21 at 23:02
  • @ChrisJ.Karr hard to say without seeing some code/examples, but the app being in draft state should not be an issue, as long as you marked it as Ready for Sandbox. Key things I've seen trip people up: "exp" too far into the future, JWT not correctly signed with the private key, and "jti" not unique (within the window of "exp"). – Ashavan May 26 '21 at 14:57