This is a placeholder for community-maintained best practices, outside of the IG publication cycle. Content here will be considered for inclusion in future publications.

(Adam Strickland,, 5/18/2022)

Hi there,

Epic recently released an OAuth 2.0 workflow that allows purely public apps (single-page or native apps) to get persistent API access. We'd like to share our method and motivation in hopes it gets added to SMART.

For some background, the following article makes a great case AGAINST the use of un-authenticated refresh token grants for public apps:

A Critical Analysis of Refresh Token Rotation in Single-page Applications

Simply put, we need some way to bind a persistent token/authorization event to a specific device. Our chosen method is public-private key cryptography, where we expect the app to bind the private key to the device such that it cannot be extracted by cross-site scripting (XSS) attacks.

 At a high level, this workflow makes use of the following:

  • The public client profile for the authorization code flow
  • Trusted dynamic client registration (Using an Initial Access Token)
  • The JWT Bearer Grant Type (urn:ietf:params:oauth:grant-type:jwt-bearer)

The one-time setup needed for this workflow is to register 2 identifiers with the authorization server:

  1. An initial client ID, one that is configured to use the public app profile for the authorization code flow
  2. A software ID, which points to a software statement on the authorization server

These 2 IDs should be 1:1 in the authorization server so that the initial authorization code flow can include consent/scope information related to the software ID (which isn't presented in the initial authorization request).

When a developer registers an initial client ID (AKA, an client ID linked to a software ID), that client ID will be able to request the a new scope we've termed system/dynamicClient.register. When the app receives this scope, it means the app is authorized to interact with the server's registration endpoint.

If an app receives this scope, it must use that access token to register a dynamic client instance immediately (only handling the initial access token in memory). The authorization server is expected to revoke that initial access token immediately on registration, so that the token cannot be re-used to register new (potentially malicious) app instances. This combination of immediate registration and revoking the initial access token is what makes this workflow secure, and ultimately allows app developers to create purely public apps without the need of an intermediate server to sign software statements.

When calling the server's register endpoint, the app provides a public key formatted as a JSON Web Key Set as the jwks request parameter. It also provides the software_id it registered with the authorization server. This registration event will return a client ID to the app instance, one that has been bound to the public key the app provided.

The authorization server is expected to tie any relevant user + clinical context/scope choices to the initial access token used in the registration event, and to persist that context to the newly created client ID. This means that the user only needs to conduct one login flow (the one that delivered the initial access token) and have their choices be respected in the dynamic client's persistent access.

Finally, the app is able to sign assertions using the private key associated with it's public key, using the JWT Bearer Grant Type (urn:ietf:params:oauth:grant-type:jwt-bearer). If the app has done it's part to bind the private key to the device, the app is able to safely request persistent API access. Since the dynamic client ID is associated with a specific authorization event, the app can use it's own client ID as the "sub" parameter rather than providing a user specific value.

A Critical Analysis of Refresh Token Rotation in Single-page Applications

A Critical Analysis of Refresh Token Rotation in Single-page Applications

  • No labels