Authorization flows are the standardized sequences (from OAuth 2.0 and OpenID Connect) that apps use to obtain tokens for calling FHIR APIs under the SMART on FHIR framework. Each flow defines how a client app is identified, how a user (if present) is authenticated and asked for consent, which scopes are granted, and how the app ultimately receives an access token (and sometimes an ID token and refresh token).
In practice, the right flow depends on whether a human user is actively launching the app (interactive launch) or the integration is system-to-system (no user present), and whether the client is confidential (can keep a secret on a server) or public (e.g., SPAs and mobile apps). Interactive launches typically use the Authorization Code flow; for public clients, PKCE is recommended to protect the code exchange. System integrations typically use SMART Backend Services, which relies on Client Credentials with a signed JWT (RFC 7523) to authenticate the calling service
Opala supports both families of flows described above. Older or low-trust patterns such as Implicit and Resource Owner Password Credentials (ROPC) are discouraged and are disabled in Opala’s Production environments; they’re documented here for completeness only.
The following flows are intended for launching an application in an interactive way: for example, when the user is logging into the SMART server via an app that is requesting access to clinical data. Each flow is discussed in more detail later in this topic.
Types of interactive launch flows:
Cross-Organization Data Access Profile (CODAP) lets a trusted external system assert a user’s identity without a user login at the authorization server. Opala does not support CODAP today; the content on this page related to this flow is reference-only.
Client Credentials (shared secret) flow is where a backend service obtains an access token by posting its client_id and confidential client secret to the token endpoint. Use only in trusted server-side environments.
SMART Backend Services (JWT client credentials) flow is recommended for system-to-system integrations. The client signs a short-lived JWT with its private key and presents it to the token endpoint as a client assertion (RFC 7523). The issued access token represents the system’s own permissions (e.g., system/*.read).
Refresh Token grant flow exchanges a refresh token for a new access token. This is typically used for interactive apps. For system flows, short-lived access tokens are a best practice which means servers typically expect clients to request new tokens as needed.
This flow is the most common for real users (not systems) who need to authenticate/authorize themselves for access to an application. It supports confidential clients (secret required) and public clients (no secret required) where a secret is a password that isi specific to the application itself, not the user.
Confidential clients can be web or mobile applications where a secured “backend server” exists and is able to handle the code exchange with the authorization server. These clients use a client secret to authenticate with the Authorization Server.
Public clients do not require a client secret. Examples of public clients include:
Opala recommends public clients use Proof Key for Code Exchange (PKCE) as described below. PKCE is an additional layer of security intended to compensate for the lack of a client secret.
In general, the Authorization Code flow should follow the below steps:
GET https://{baseurl}/oauth/authorize
?response_type=code
&scope=openid%20profile%20patient/*.read
&client_id={client_id}
&state={state}
&redirect_uri={redirect_uri}
[&code_challenge={code_challenge}&code_challenge_method=S256]
Note the parameters in the URL:
code for the Authorization Code flow.login value.Note: If using PKCE, the URL will also include code_challenge and code_challenge_method parameters.
https://myapp.example.com/cb?state=af0ifjsldkj&code=cdcd883gr3g8dggakH
The client should verify the state matches its original request to prevent replay attacks.
The code parameter is a temporary Authorization Code the SMART Application uses to request an Access Token.
POST https://{baseurl}/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64({client_id}:{client_secret})
grant_type=authorization_code&
code={authorization_code}&
redirect_uri={redirect_uri}&
scope=openid%20profile
Public client (PKCE, no client secret; include code_verifier):
POST https://{baseurl}/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code={authorization_code}&
redirect_uri={redirect_uri}&
client_id={client_id}&
code_verifier={code_verifier}
Note the headers:
application/x-www-form-urlencoded.Basic followed by the Base64-encoded [client ID]:[client secret].Note the parameters:
authorization_code.Note: If using PKCE, include code_verifier as well.
The Authorization Server responds with:
{
"access_token": "eyJraWQiOiJ1.eyJhenAiOiJteS1jbGllb.Bv42OB0p",
"id_token": "eyJra33ed8dofh.doh3ohfeisgdOiJteS1jbGllb.u44hdhB0p",
"token_type": "bearer",
"scope": "openid profile patient/*.read"
}If the client requested offline_access or online_access scopes, a Refresh Token is also included:
{
"access_token": "eyJraWQiOiJ1.eyJhenAiOiJteS1jbGllb.Bv42OB0p",
"id_token": "eyJra33ed8dofh.doh3ohfeisgdOiJteS1jbGllb.u44hdhB0p",
"token_type": "bearer",
"expires_in": 59,
"scope": "openid profile patient/*.read",
"refresh_token": "dd94j4hdsjrbfyf8i4ir9fizJHHfhdg"
}
When using public clients with the Authorization Code flow, Opala highly recommends using the Proof Key for Code Exchange extension (also known as RFC7636/PKCE and pronounced “Pixy”) in order to mitigate potential interception attacks. PKCE allows the client to add an additional token to the initial authorization request (Step 1 above) and then requires during the code exchange step (Step 4 above) that the client submit a verifier which can be used as additional proof that no “man in the middle” has intercepted the authorization code and is trying to maliciously redeem it.
In order to use PKCE, the client should first generate a random string to use as a challenge. For example:
d93i4uggdiuf8p4or0Au
Note: Do not reuse this string. A new random string must be generated each time PKCE is used.
The client then generates a SHA-256 hash of this string and Base64 encodes the result. For the example above, this results in:
6VLmPKYqeh3cI/YKXLbeOLfF0SiR3/38pQC6ozldmXs=
The following additions to the steps described above are then performed: When the SMART Application redirects the user to the Authorization Server, two new parameters are added as shown in the following example:
GET https://{baseurl}/oauth/authorize
?response_type=code
&scope=openid%20profile%20patient/*.read
&client_id={client_id}
&state={state}
&redirect_uri={redirect_uri}
&code_challenge={code_challenge}
&code_challenge_method=S256
Note the two additional parameters:
S256 to specify SHA-256 hashing. Though PLAIN is also allowed, it is not recommended for security reasons.When performing the code exchange, one new parameter is added as shown in the following example:
POST https://{baseurl}/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&
code={authorization_code}&
redirect_uri={redirect_uri}&
client_id={client_id}&
code_verifier={code_verifier}
Note the additional parameter:
This flow is used for applications that are unable to “keep a secret.” It should only be used in these cases.
The SMART Application redirects the user to the Authorization Server (or in the case of a native application, opens a web browser) at a URL similar to the following:
GET https://{baseurl}/oauth/authorize
?response_type=id_token%20token
&scope=openid%20profile%20patient/*.read
&client_id={client_id}
&state={state}
&redirect_uri={redirect_uri}
The Authorization Server presents the user with a login screen, and if necessary asks the user to approve the selected scopes.
The Authorization Server redirects the user back to the Callback URL, adding several parameters to the URL. For example:
https://{redirect_uri}
#state=af0ifjsldkj
&access_token=cdcd883gr3g8d.ggakHeuedhd
&token_type=bearer
&expires_in=60
In this response the access_token parameter contains the Access Token that has been granted.
This flow relies on credentials being collected directly in the source application and then transferred to the Authorization Server for validation.
To perform the Resource Owner Password Credentials flow, the application collects the user’s username and password directly in the Application UI. It then uses an HTTP POST to the token endpoint in order to request an access token.
The following example shows a SMART Application making a request to the Authorization Server to grant a new Access Token using the Resource Owner Password Credentials flow (for readability each parameter is shown on a new line but in a real request these would be joined together in one long line):
POST /oauth/token
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=password
&client_id=growth_chart
&client_secret=someclientsecret
&username=someuser
&password=someuserspassword
&scope=openid+profile+patient.%2A%2Fread
This client request must contain the client secret if the client definition has one.
{
"access_token":"eyJraWQiOiJ1.eyJhenAiOiJteS1jbGllb.Bv42OB0p",
"id_token":"eyJra33ed8dofh.doh3ohfeisgdOiJteS1jbGllb.u44hdhB0p",
"token_type":"bearer",
"expires_in":59,
"scope":"openid profile patient/*.read"
} If the client definition contains a client secret, this secret must be provided during the grant process. This should only be done if the communication between the client application and the Authorization Server is being performed using a trusted and secured backend server.
If the client definition does not contain a client secret, this flow will be permitted without the client secret as a request parameter. Use this mode with caution as it has security implications which must be considered.
Opala does not support CODAP today; the following describes how CODAP works for reference.
The SMART Cross-Organization Data Access Profile (CODAP) is useful in scenarios where data is being requested from a trusted third-party application that maintains its own identity provider and does not wish to federate at the directory level.
This flow is fundamentally different from the interactive flows in that it allows the third-party application to securely assert the identity of the requesting system and user. In turn, Opala will issue a valid Access Token (and create entries in its own user database in order to track audit events), but will not attempt to verify the identity of the calling user at all.
The Cross-Organization Data Access Profile uses two separate tokens which are generated and signed by the third-party application. The technology for this signature involves JSON Web Tokens. For this exchange to be secure, the tokens are signed using a private key that only the third-party application has access to, and are verified using a corresponding public key.
These tokens are:
Although the specification treats these as separate tokens in order to facilitate flows where these tokens are generated in separate steps, it is also possible to generate a single token containing all relevant assertions. This can reduce the burden on implementors.
The following describes the Cross-Organization Data Access Profile flow steps:
The user logs in
The user authenticates with the third-party system. This could be an EMR, HIS, web portal, etc., using whatever credentials that system supports. This flow assumes that the EHR is able to identify the user and make strong assertions about their identity. This fact is key to understanding the difference between this flow and the other SMART auth flows: for all other flows, the user/system is authenticating with Opala. For this flow, the third-party system is simply asserting the identity of the user/system that will be making requests.
The EHR Generates an Authorization Token
Per the CODAP specification, the authorization token will be a signed JWT with claims resembling the following example. Note that the claims include information about who the user is, whose data are they requesting, and why are they requesting it.
{
"iss": "http://third-party-ehr/issuer",
"sub": "someuser",
"aud": "https://fhir-server/issuer",
"acr": "http://nist.gov/id-proofing/level/3",
"jti": "uuid-representing-token",
"iat": 1418698788,
"exp": 1422568860,
"requested_record": {
"resourceType": "Patient",
"name": {
"text": "Pauline Smith"
},
"identifier": [
{
"system": "http://example.org/patients",
"Identifier": "0123456789"
}
],
"gender": "female",
"birthDate": "1970-05-18"
},
"reason_for_request": "treatment",
"requested_scopes": "patient/*.read patient/*.write",
"requesting_practitioner": {
"resourceType": "Practitioner",
"identifier": [
{
"system": "http://example.org/providers",
"value": "983736235"
}
],
"name": {
"text": "Juri van Gelder"
},
"practitionerRole": [
{
"role": {
"coding": [
{
"system": "http://snomed.info/sct",
"code": "36682004",
"display": "Physical therapist"
}
]
}
}
]
}
}
The "requested record", which is used by the requesting EHR to identify the patient whose record is being accessed, is a simple FHIR Patient resource. The level of identification required is not a requirement. For example, you may choose to require only an identifier, or a name and birth date, etc.
Similarly, the "requesting practitioner" is a FHIR Practitioner resource. The level of identification required is not a requirement.
Generate Authentication Token
Per the CODAP specification, the authentication token will be a signed JWT containing claims resembling the example below. Note that where the authorization token contained claims about the user and what they wanted to accomplish, this token contains information about the system actually performing the request.
{
"iss": "http://third-party-ehr/issuer",
"sub": "{client_id}",
"aud": "https://{baseurl}/issuer",
"iat": 1422568860,
"expires_in": 1438698788,
"jki": "uuid-representing-token",
"kid": "key_id"
}
Third-Party System issues Token Request
This step consists of an HTTP POST from the third-party application to the CODAP Auth Server. The request will resemble the following:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer
assertion={signed authorization JWT}
&
client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion={signed authentication JWT}
In the above example, the {signed authorization JWT} and {signed authentication JWT} should be replaced with signed versions of the JWTs described above, using a key that belongs to the third-party application.
Invoke Callback Scripts and generate Access Token
In this step, a custom callback script is executed that validates the information supplied by the authorization token and performs any necessary processing for the specific login workflow being implemented. This step is very flexible from implementation to implementation. For example, one implementation might decide that only minimal (or no) details about the patient are required, where another implementation might decide that a complete set of demographics are required to be supplied by the third-party application. This is completely implementation dependent and is specified in the callback script.
Third-Party System issues Token Request
Once the validation step has completed, an Access Token is generated and returned to the user. This takes the form of a standard OAuth2 token response, such as the one below.
200 OK
Content-Type: application/json
{
"access_token": "iaslfhsfauel3fgterioioeg0o.dfoifsiou.ururhi"
}
Use Access Token
Once the third-party application has an Access Token, it may be used to perform any REST calls that are authorized. A simple FHIR read operation is shown as an example below.
GET /Patient/123 HTTP/1.1
Authorization: Bearer iaslfhsfauel3fgterioioeg0o.dfoifsiou.ururhi
Accept: application/fhir+json
The Client Credentials flow can be used to authenticate a client directly, without the involvement of any user account.
This is useful in situations where a system needs to be authorized for actions without needing to involve or authorize a user. For example, this grant type could be used for authenticating a system for system-to-system data flow.
The following example shows a SMART Application making a request to the Authorization Server to grant a new Access Token using the Client Credentials Grant (for readability each parameter is shown on a new line but in a real request these would be joined together in one long line):
POST /oauth/token
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=growth_chart
&client_secret=someclientsecret
The server will then respond with a response similar to the following, which includes a new Access Token (note that the Authorization Server may or may not reuse the same Refresh Token at its own discretion).
{
"access_token":"eyJraWQiOiJ1bml0dG.eyJhenAiOiJteS1j.THbNnfuX7Ly4g",
"token_type":"bearer",
"expires_in":59,
"scope":"patient/*.read"
}This variant of the Client Credentials flow (see above) uses a cryptographically signed JWT as the credential that the client presents to the authorization server, instead of a client secret. This type of authentication is specified as the mechanism for SMART Backend Services and is described in [RFC 7523](JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants) JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication and Authorization Grants.
In this flow, the client first creates a JWT resembling the following:
{
"jti":"cc7a3074-5c59-4e3f-b493-bb5115049344",
"sub":"my-client-id",
"iss":"my-client-id",
"aud":"http:\/\/example.com\/issuer\/oauth\/token",
"kid":"some-key-id",
"exp":1623019405,
"iat":1623019345
}
Note the following claims in the JWT document:
The JWT is then digitally signed, and passed to the Authorization Server in a request similar to the following:
POST /oauth/token
Accept: application/json
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_assertion_type=urn:ietf:params:oauth:grant-type:jwt-bearer
&client_assertion=[serialized JWT]
The server will then respond with a response similar to the following, which includes a new Access Token (note that the Authorization Server may or may not reuse the same Refresh Token at its own discretion).
{
"access_token":"eyJraWQiOiJ1bml0dG.eyJhenAiOiJteS1j.THbNnfuX7Ly4g",
"token_type":"bearer",
"expires_in":59,
"scope":"patient/*.read"
}
The OpenID Connect flow allows for an Authorization Server to issue Access Tokens that are then consumed by Resource Servers in order to authenticate access to APIs. The design of the protocol means that this flow can generally work with minimal direct interaction needed between the Resource Server and the Authorization Server. Tokens leverage digital signatures, meaning that the Resource Server can verify that a token was issued by the Authorization Server without actually needing to communicate with it.
This is a useful property of the process as it means that the two different logical servers have less (or no) need to communicate directly with each other in order to validate individual client requests. This is good for scalability (indeed this is a common design principle in the popular Microservice Architecture application design).
However, this property does have one important drawback: it means that the Authorization Server is not able to notify the Resource Server if it wishes to revoke an access token. Because of this, the Authorization Server will often specify an access token with a short expiry time (often as short as 60 seconds or less) and will issue a second token called a Refresh Token while it is issuing an access token to the SMART Application.
This Refresh Token may be used by the SMART Application to request a new access token when the current one has expired (or perhaps beforehand). By issuing Access Tokens with a short expiry time and regularly requiring the SMART Application to request a new Access Token, the Authorization Server is able to ensure that the SMART Application only has a valid Access Token for an appropriate amount of time (i.e., the Authorization Server can effectively revoke tokens within a short amount of time).
Refresh tokens may also be used by the application to perform background tasks, such as periodic fetches of data on behalf of the user, even when the user is not actively using the client application.
A client can request that a Refresh Token be issued by requesting the offline_access or online_access scopes.
If one of these scopes is approved for the client session, the refresh token will be returned to the client during the Authorization Code flow (see above).
Once a client has a Refresh Token, the token can be exchanged for a new access token using the OAuth2 Refresh Flow.
The following example shows a SMART Application making a request to the Authorization Server to grant a new Access Token using a Refresh Token (for readability each parameter is shown on a new line but in a real request these would be joined together in one long line):
POST /oauth/token
Accept: application/json
Content-Type: application/x-www-form-urlencoded
Authorization: Basic [client_id:client_secret]
grant_type=refresh_token
&refresh_token=37d53025-964c-4ab4-afcb-54ea5d892ebb
&client_id=growth_chart
The server will then respond with a response similar to the following, which includes a new Access Token and a Refresh Token that may be used to request the next Access Token (note that the Authorization Server may or may not reuse the same Refresh Token at its own discretion).
{
"access_token":"eyJraWQiOiJ1bml0dG.eyJhenAiOiJteS1j.THbNnfuX7Ly4g",
"token_type":"bearer",
"refresh_token":"c5bbdcab-2e02-4c80-bb2c-3022c66b2a7e",
"expires_in":59,
"scope":"patient/*.read online_access openid"
}
In this case, the client must present a valid Access Token instead. The following example shows such a request. Note that the Client ID is not presented.
POST /oauth/token
Accept: application/json
Authorization: Bearer zddefffpof4wopwjfsf.sfsef9ef9oesjKLKLhldhdg
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=37d53025-964c-4ab4-afcb-54ea5d892ebbNote: If the client definition is not marked as Client Secret Required to Authenticate, it will be possible for the client to request a new access token without requiring the use of the client secret.