JSON Web Tokens (JWTs)
Background
JSON Web Tokens (JWTs) can be used for some OAuth 2.0 workflows.
JWTs are required to be used by the client credentials flow used by backend services, and can be used, as an alternative to client secrets, for apps that use refresh tokens.
You will generate a one-time use JWT to authenticate your app to the authorization server and obtain an access token that can be used to authenticate your app's web service calls, and potentially a refresh token that can be used to get another access token without a user needing to authorize the request.
There are several libraries for creating JWTs. See jwt.io for some examples.
Creating a Public Private Key Pair for JWT Signature
There are several tools you can use to create a key pair.
As long as you can export your public key to a base64 encoded X.509 certificate for registration on the Epic on FHIR website, the tool you use to create the key pair and file format used to store the keys is not important.
If your app is hosted by Epic community members ("on-prem" hosting) instead of cloud hosted you should have unique key pairs for each Epic community member you integrate with. In addition, you should always use unique key pairs for non-production and production systems.
Here are examples of two tools commonly used to generate key pairs:
Creating a Public Private Key Pair: OpenSSL
You can create a new private key named privatekey.pem using OpenSSL with the following command:
openssl genrsa -out /path_to_key/privatekey.pem 2048
Make sure the key length is at least 2048 bits.
Then you can export the public key to a base64 encoded X.509 certificate named publickey509.pem using this command:
openssl req -new -x509 -key /path_to_key/privatekey.pem -out /path_to_key/publickey509.pem -subj '/CN=myapp'
Where '/CN=myapp' is the subject name (for example the app name) the key pair is for.
The subject name does not have a functional impact in this case but it is required for creating an X.509 certificate.
Creating a Public Private Key Pair: Windows PowerShell
You can create a key pair using Windows PowerShell with this command, making sure to run PowerShell as an administrator:
New-SelfSignedCertificate -Subject "MyApp"
Note that the Epic on FHIR website is extracting the public key from the certificate and discarding the rest of the certificate information, so it's not 'trusting' the self-signed certificate per se.
The subject name does not have a functional impact in this case but it is required for creating an X.509 certificate.
You can export the public key using either PowerShell or Microsoft Management Console.
If you want to export the public key using PowerShell, take note of the certificate thumbprint and storage location printed when you executed the New-SelfSignedCertificate command.
PS C:\dir> New-SelfSignedCertificate -Subject "MyApp"
PSParentPath: Microsoft.PowerShell.Security\Certificate::LocalMachine\MY
Thumbprint Subject
---------- -------
3C4A558635D67F631E5E4BFDF28AE59B2E4421BA CN=MyApp
Then export the public key to a X.509 certificate using the following commands
PS C:\dir> $cert = Get-ChildItem -Path Cert:\LocalMachine\My\3C4A558635D67F631E5E4BFDF28AE59B2E4421BA
PS C:\dir> Export-Certificate -Cert $cert -FilePath newpubkeypowerShell.cer
Export the binary certificate to a base64 encoded certificate:
- PS C:\dir> certutil.exe -encode newpubkeypowerShell.cer newpubkeybase64.cer
If you want to export the public key using Microsoft Management Console, follow these steps:
1.Run (Windows + R) > mmc (Microsoft Management Console).
2.Go to File > Add/Remove SnapIn.
3.Choose Certificates > Computer Account > Local Computer. The exact location here depends on the certificate store that was used to create and store the keys when you ran the New-SelfSignedCertificate PowerShell command above. The store used is displayed in the printout after the command completes.
4.Then back in the main program screen, go to Personal > Certificates and find the key pair you just created in step 1. The "Issue To" column will equal the value passed to -Subject during key creation (e.g. "MyApp" above).
5.Right click on the certificate > All Tasks > Export.
6.Choose option to not export the private key.
7.Choose to export in base-64 encoded X.509 (.CER) format.
8.Choose a file location.
Finding the Public Key Certificate Fingerprint (Also Called Thumbprint)
The public key certificate fingerprint (also known as thumbprint in Windows software) is displayed for JWT signing public key certificates that are uploaded to the Epic on FHIR website.
There are a few ways you can find the fingerprint for a public key certificate:
If you created the public key certificate using Windows PowerShell using the steps above, the thumbprint was displayed after completing the command.
You can also print public keys and their thumbprints for a given certificate storage location using the Get-ChildItem PowerShell command.
For example, run Get-ChildItem -Path Cert:\LocalMachine\My to find all certificate thumbprints in the local machine storage.
You can follow the steps here to find the thumbprint of a certificate in Microsoft Management Console.
You can run this OpenSSL command to print the public key certificate fingerprint that would be displayed on the Epic on FHIR website, replacing openssl_publickey.cert with the name of your public key certificate:
$ openssl x509 -noout -fingerprint -sha1 -inform pem -in openssl_publickey.cert
Note that the output from this command includes colons between bytes which are not shown on the Epic on FHIR website.
Creating a JWT
The JWT should have these headers:
Header | Required/Optional | Description |
---|---|---|
alg | required | The JWT authentication algorithm. Currently only RSA signing algorithms are supported so RSA 384 should be used and this should be set to RS384. |
typ | required | This should always be set to JWT. |
kid | required | For apps using JSON Web Key Sets (including dynamically registed clients), set this value to the kid of the target public key from your key set |
jku | optional | For apps using JSON Web Key Set URLs, optionally set this value to the URL you registered on your application |
The JWT header should be formatted as follows:
{ "alg": "RS384", "typ": "JWT" } The JWT should have these claims in the payload:
Claim | Required/Optional | Description | Remarks |
---|---|---|---|
iss | Required | Issuer of the JWT. This is the app's client_id, as determined during registration on the Epic on FHIR website, or the client_id returned during a dynamic registration | This is the same as the value for the sub claim. |
sub | Required | Issuer of the JWT. This is the app's client_id, as determined during registration on the Epic on FHIR website, or the client_id | required |
aud | Required | The FHIR authorization server's token endpoint URL. This is the same URL to which this authentication JWT will be posted. See below for an example POST. | It's possible that Epic community member systems will route web service traffic through a proxy server, in which case the URL the JWT is posted to is not known to the authorization server, and the JWT will be rejected. For such cases, Epic community member administrators can add additional audience URLs to the allowlist, in addition to the FHIR server token URL if needed. |
jti | Required | A unique identifier for the JWT. | The jti must be no longer than 151 characters and cannot be reused during the JWT's validity period, i.e. before the exp time is reached. |
exp | Required | Expiration time integer for this authentication JWT, expressed in seconds since the "Epoch" (1970-01-01T00:00:00Z UTC). | The exp value must be in the future, and can be no more than 5 minutes in the future at the time the access token request is received. |
nbf | Required | Time integer before which the JWT must not be accepted for processing, expressed in seconds since the "Epoch" (1970-01-01T00:00:00Z UTC). | The nbf value cannot be in the future, cannot be more recent than the exp value, and the exp - nbf difference cannot be greater than 5 minutes. |
iat | Required | Time integer for when the JWT was created, expressed in seconds since the "Epoch" (1970-01-01T00:00:00Z UTC). | The iat value cannot be in the future, and the exp - iat difference cannot be greater than 5 minutes. |
Here's an example JWT payload:
{
"iss": "d45049c3-3441-40ef-ab4d-b9cd86a17225",
"sub": "d45049c3-3441-40ef-ab4d-b9cd86a17225",
"aud": "https://fhir.epic.com/interconnect-fhir-oauth/oauth2/token",
"jti": "f9eaafba-2e49-11ea-8880-5ce0c5aee679",
"exp": 1583524402,
"nbf": 1583524102,
"iat": 1583524102
}
- The header and payload are then base64 URL encoded, combined with a period separating them, and cryptographically signed using the private key to generate a signature. Conceptually:
signature = RSA-SHA384(base64urlEncoding(header) + '.' + base64urlEncoding(payload), privatekey)
The full JWT is constructed by combining the header, body and signature as follows:
base64urlEncoding(header) + '.' + base64urlEncoding(payload) + '.' + base64urlEncoding(signature)
A fully constructed JWT looks something like this:
eyJhbGciOiJSUzM4NCIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJkNDUwNDljMy0zNDQxLTQwZWYtYWI0ZC1iOWNkODZhMTcyMjUiLCJzdWIiOiJkNDUwNDljMy0zNDQxLTQwZWYtYWI0ZC1iOWNkODZhMTcyMjUiLCJhdWQiOiJodHRwczovL2ZoaXIuZXBpYy5jb20vaW50ZXJjb25uZWN0LWZoaXItb2F1dGgvb2F1dGgyL3Rva2VuIiwianRpIjoiZjllYWFmYmEtMmU0OS0xMWVhLTg4ODAtNWNlMGM1YWVlNjc5IiwiZXhwIjoxNTgzNTI0NDAyLCJuYmYiOjE1ODM1MjQxMDIsImlhdCI6MTU4MzUyNDEwMn0.dztrzHo9RRwNRaB32QxYLaa9CcIMoOePRCbpqsRKgyJmBOGb9acnEZARaCzRDGQrXccAQ9-syuxz5QRMHda0S3VbqM2KX0VRh9GfqG3WJBizp11Lzvc2qiUPr9i9CqjtqiwAbkME40tioIJMC6DKvxxjuS-St5pZbSHR-kjn3ex2iwUJgPbCfv8cJmt19dHctixFR6OG-YB6lFXXpNP8XnL7g85yLOYoQcwofN0k8qK8h4uh8axTPC21fv21mCt50gx59XgKsszysZnMDt8OG_G4gjk_8JnGHwOVkJhqj5oeg_GdmBhQ4UPuxt3YvCOTW9S2vMikNUnxrhdVvn2GVg
JSON Web Key Sets
Applications using JSON Web Token (JWT) authentication can provide their public keys to Epic as a JSON Web Key (JWK) Set. Each key in the set should be of type RSA and have the kty, n , e and kid fields present:
{
"keys": [
{ "kty":"RSA", "e": "AQAB", "kid": "d1fc1715-3299-43ec-b5de-f943803314c2", "n": "uPkpNCkqbbismKNwKguhL0P4Q40sbyUkUFcmDAACqBntWerfjv9VzC3cAQjwh3NpJyRKf7JvwxrbELvPRMRsXefuEpafHfNAwj3acTE8xDRSXcwzQwd7YIHmyXzwHDfmOSYW7baAJt-g_FiqCV0809M9ePkTwNvjpb6tlJu6AvrNHq8rVn1cwvZLIG6KLCTY-EHxNzsBblJYrZ5YgR9sfBDo7R-YjE8c761PSrBmUM4CAQHtQu_w2qa7QVaowFwcOkeqlSxZcqqj8evsmRfqJWoCgAAYeRIsgKClZaY5KC1sYHIlLs2cp2QXgi7rb5yLUVBwpSWM4AWJ_J5ziGZBSQYB4sWWn8bjc5-k1JpUnf88-UVZv9vrrkMJjNam32Z6FNm4g49gCVu_TH5M83_pkrsNWwCu1JquY9Z-eVNCsU_AWPgHeVZyXT6giHXZv_ogMWSh-3opMt9dzPwYseG9gTPXqDeKRNWFEm46X1zpcjh-sD-8WcAlgaEES6ys_O8Z" },
{ "kty":"RSA", "e":"AQAB","kid":"1248110c-afbd-484c-b75b-b30200ffcf05","n":"zYlmlLzhQNpRq263CoShHgyJCrNLm6oFrf0FTQ0pwYIyaTFZirW-G6wDyVwtaQA5Kjth0NjglQIYfPR9rPBGmX3CSVtmZCLEhFeMMp_W3SoZFZJJypP4x2AbG7TqPvkbpuYWHT15Lvp9amgGqdNQ-UyPZS8WMp1NloDw4R9Nomo8yEk8OcgmG1ORqrmFLam3AHPvZZH2wp88gE9DWOAoOwOpDP32QSo1ii0Wrw2gV_UwsVuAjhkFK3asOCHoqlRhm_c49d1OgB7QvrnP-S5-Wa1JsQ5lKa52G7UVkyNPA1pFw7iW7PHXCgoe8inSmnrqkzmtmDjpOnqpuGU39I6byQ"}
]
}
Key Identifier Requirements
Epic recommends your application provide the kid field in JWK Sets and in your JWT Headers, since Epic can use the identifier to find your intended key. Epic does not require your key identifiers to be thumbprints, and will accept any value that is unique within your key set.
Hosting a JWK Set URL
Starting in the May 2021 version of Epic, apps using JWT authentication can use a JSON Web Key Set URL to provide public keys to Epic for JWT verification. JWK Set URLs streamline implementation and make key rotation feasible by providing a centralized and trusted place where Epic community members can retrieve your public keys.
Note that you will still need to provide static public keys to community members on Epic versions earlier than the May 2021 version.
App-Level vs Licensing Specific JWK Set URLs
By default, every community member will use the app-level JWK Set URLs when validating your application's JWTs. These are the URLs you provide when building your application. Re-using these default URLs for all your integrations is appropriate for cloud-based apps that can re-use a single private key (without making a copy).
However, if your application is hosted by Epic community members (“on-prem”, applications), then you should instead provide different JWK Set URLs for every licensed community member. There are 2 reasons on-prem applications should use distinct URLs:
These applications require physically separate servers, and cannot share a private key across installations without copying and transporting the key (a security risk)
Attempting to aggregate multiple public keys behind one URL is a security risk if those keys are in physically different places. If any one of your server environments is compromised, it can be used to impersonate any other instance of your application
Registering a JWK Set URL
When building or licensing an application that will authenticate against Epic, you are prompted to optionally upload non-production and production JWK Set URLs to your app. When registering these URLs, be sure of the following:
- Are secured with TLS
- Are publicly accessible
- Do not require authentication
- Will not change over time
- Are responsive
- Epic will verify points 1-3 whenever you provide a JWK Set URL
Note that there are separate non-production and production URLs because we recommend you use separate key sets for each.
Key Rotation
Before rotating your keys, remove any static public keys from your existing downloads
One benefit of JWK Set URLs is that your application can rotate its key as needed, and Epic can dynamically fetch the updated keys. It is a best practice to periodically rotate your private keys, and if you choose to do so, we recommend the following strategy:
- Create your new public-private key pair ahead of time
- Add your new public key to your JWK Set in addition to the existing one
- Start using your new private key for signing JWTs
- Once you are certain your system is no longer using the old private key, wait 5 minutes (the maximum JWT expiration time) remove the old public key from your JWK Set
In between steps 3 and 4, Epic will notice you are using a new private key and will automatically fetch the new key set. Every time you present a JWT to Epic we will check your JWK Set URL at most once, and only if the cached keys are expired or did not verify your JWT
- Epic's default and maximum cache time is 29 hours, but your endpoint can override the default by using the Cache-Control HTTP response header.
- This is most useful during development and testing when you may want to update your non-production public keys ad-hoc, or to test the availability of your server from Epic.
- You could for instance respond with "Cache-Control": "no-store" to stop Epic from storing your keys at all. This is not recommended for production usage since you lose the performance benefits of caching.
Providing Multiple Public Keys
How
Apps can provide multiple public keys to an Epic community member by:
Providing multiple static public keys
Providing a JWK Set URL that contains multiple public keys
Why Not?
In general, the only reason to provide multiple public keys is if your system spans multiple separate servers or cloud environments, and so you cannot share a private key between them.
If you can re-use a key without making copies of it, then you should re-use that key and focus on protecting it with the best policy available (For example, using your hosting provider's key store or using a Trusted Platform Module).
Use Cases
Example use cases:
- Having multiple isolated testing environments, where you may provide multiple non-production public keys (one for each environment)
- On-premises applications with multiple servers hosted by a given community member. In this case you would provide multiple production public keys (one for each server)