/images/resources/tutorials/advanced/jwt-assertions.png

JWT Assertions

On this page

Client and User Authentication using JWTs

There is an extension to the OAuth standard defined in RFC 7523, that specifies how JSON Web Tokens (JWTs) can be used to authenticate users and clients. This spec is based on RFC 7521, more general, one for using assertions of various kinds. It is also profiled (i.e., further specified) by OpenID Connect. All this makes it very confusing to understand, so this tutorial seeks to clarify how JWTs can be used according to these standards. For the purpose of this tutorial it's important to see clearly how these specs fit together:

JWT Authentication-related Specifications

Then, it is important to realize that the OAuth JWT profile addresses two different use cases:

  1. User authentication
  2. Client authentication

The OpenID Connect profile only refines how client authentication is done; it does not address user authentication using JWTs. Also, these two cases can be used together or independently. Both of these take place at the OAuth server's token endpoint, but the latter can occur at other endpoint (e.g., introspection). If both are used in tandem, the browser-based exchanges that flow through the authorization endpoint are completely unused. This means that a couple of JWTs can be sent to the token endpoint to perform both user and client authentication. If a JWT is only used for authenticating the client though, the browser-based redirection will still be used to obtain user authentication and authorization. If the client authenticates with some other kind of credential, the request will only contain a JWT that authenticates the user. Allowing all these permutations requires various parameters to be provided in requests made to Curity. The details are described in the next subsections.

Client Authentication using JWTs

Clients must authenticate to Curity before they are allowed to perform certain requests. For example, a client must prove its identity before it can exchange an authorization code at the token endpoint. Likewise, a client must authenticate before it can introspect a token. This is required to stifle multiple security threats. There are various ways a client may do this. The most common is a client ID and secret. These are passed either in the request body or an HTTP Authorization header using the "basic" authentication scheme defined in RFC 7617. Another mechanism is by using a JWT; the particulars of how this JWT is sent and what it must contain is what RFC 7521 and OpenID Connect define. To understand this clearly, one must first know a few things about JWTs, clients and the OAuth server.

JWT Format

Briefly, an unencrypted, signed JWT consists of three parts:

  1. The header;
  2. The body or payload; and
  3. A signature of the payload.

The header may include information about the key and algorithm used to sign the payload. This is needed unless the receiver has obtained this information through other means. As an example, a JWT used for client authentication may have a header and payload like this:

json
1234
{
"alg": "RS256",
"typ": "JWT"
}

The associated payload may look like this:

json
123456789
{
"aud": "https://localhost:8443/oauth/v2/oauth-token",
"iss": "client-one",
"sub": "client-one",
"nbf": 1535806905,
"exp": 1535810505,
"iat": 1535806905,
"jti": "id123456"
}

RFC 7521 doesn't dictate the value of the issuer (iss) claim, but it does require one. OpenID Connect does, however. It mandates that the issuer and subject (sub) are the client ID. For this reason, Curity requires both claims to be present with a value of the client ID. Both standards require an audience claims (aud); OpenID Connect says it should be the OAuth server's token endpoint, whereas RFC 7523 allows it to be any identifier that represents the OAuth server. Curity only allows the audience to be the token endpoint. Similarly, the OAuth spec does not require an expiration time (exp) claim, but OpenID Connect does. Therefore, Curity requires one. The issued at (iat) and not-before (nbf) claims are optional; if present, Curity will ensure that they are valid before accepting the JWT. Including these values is recommended. The last claim in the listing above, the JWT ID (jti), is required according to OpenID Connect, but it only needs to be unique if so configured in one of Curity's OAuth profiles. In the admin UI, this is set in the Client Settings section of the General page of each Token Service Profile. Enable Asymmetrically Signed JWT and at the bottom of the page enable Enforce Unique JTI Values.

JWT ID Uniqueness Requirement

Sending a JWT to the OAuth Server

The JWT is sent to the OAuth server is form data that has been encoded into URL parameter and is posted to an endpoint, like the token or revoke concepts. RFC 7523 stipulates how this should be done. Specifically, the client should include the following parameters:

Parameter NameParameter Value
client_assertion_typeurn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertionA JWT that the client has signed

Optionally, a client_id can also be included. If so, it should match the one in the JWT; otherwise, Curity will return a 400, bad request, error.

A sample request (with extra line breaks added for clarity only) to the revoke endpoint is shown in the following listing:

http
12345678
POST /oauth/v2/oauth-revoke HTTP/1.1
Host: localhost:8443
Content-Type: application/x-www-form-urlencoded
 
token=71a337f2-6b36-4e72-bd14-dcb23a30207c&
token_type_hint=refresh_token&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2Rldi9vYXV0aC9hbm9ueW1vdXMiLCJpc3MiOiJjbGllbnQtb25lIiwic3ViIjoiY2xpZW50LW9uZSIsImV4cCI6MTUzNTgxNjkzNiwianRpIjoiMTIzNDU2In0.RZF2y5CXNhogMaDqncfY9FNxG-ufZdHpKQ0Iyi1iyOrA-S_rZ3Ni_6hn2p1LgEDMNgTPMwOQrbaZinXqjus2onXF1fAYz_HR3F0TvyjrInGuekBnCZeULVKme6QmRGMz_6wji3nuL0OPfwmrUHki-W-c_PTVQz1rfh6S8z5UgH3KTInJsaECxayphYoXMw1wyAjp1cAE4jiv5z6uUZrp5dspwL5sLt1vZ-cqNQlNIJtAD3IEvZs7rllPle47s3Ld6EOWVL2gleePwGlwObweAdm15ZbPDSV_souznuCGf8U9nMIEwvR799Rh_uodmqxSvF-TX7L1C4UragTNuvybxQ

Similarly, a sample request to the token endpoint to redeem an authorization code is shown in the following listing:

http
12345678910
POST /oauth/v2/oauth-token HTTP/1.1
Host: localhost:8443
Content-Type: application/x-www-form-urlencoded
 
response_type=token&
grant_type=authorization_code&
code=hLlEHgINssCPamfHE6ua0llCMGwcdUib&
redirect_uri=https%3A%2F%2Flocalhost%2Fclient-one%2Fcb1&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
client_assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2Rldi9vYXV0aC9hbm9ueW1vdXMiLCJpc3MiOiJjbGllbnQtb25lIiwic3ViIjoiY2xpZW50LW9uZSIsImV4cCI6MTUzNTgxNjkzNiwianRpIjoiMTIzNDU2In0.RZF2y5CXNhogMaDqncfY9FNxG-ufZdHpKQ0Iyi1iyOrA-S_rZ3Ni_6hn2p1LgEDMNgTPMwOQrbaZinXqjus2onXF1fAYz_HR3F0TvyjrInGuekBnCZeULVKme6QmRGMz_6wji3nuL0OPfwmrUHki-W-c_PTVQz1rfh6S8z5UgH3KTInJsaECxayphYoXMw1wyAjp1cAE4jiv5z6uUZrp5dspwL5sLt1vZ-cqNQlNIJtAD3IEvZs7rllPle47s3Ld6EOWVL2gleePwGlwObweAdm15ZbPDSV_souznuCGf8U9nMIEwvR799Rh_uodmqxSvF-TX7L1C4UragTNuvybxQ

Note the last two lines of each of these listings. These include the two parameters from the table above and are what Curity looks for when authenticating the client using this protocol.

Authenticating the Client

To authenticate the client using a JWT, the third part mentioned above, the signature, is used. To this end, Curity obtains the iss claim from the payload of the JWT. If the value corresponds to a known client, it will determine if that client has been configured to allow authentication using a private key. If so, the corresponding public key will used to check the signature. To do this, the payload of the JWT will be signed using the client's public key and the algorithm provided in the header of the JWT. If the result matches the included signature, then the client will be authenticated.

To configure this in Curity using the admin UI, do the following:

  1. Upload the client's public key as a signature verification key.
  2. Select an OAuth profile from the Token Service tab.
  3. From Clients link on the sidebar, select the client to modify or add a new one.
  4. After a capability has been added to the client that requires some form of authentication, you will be able to specify which type to use. Select asymmetric-key and specify the signature verification key (uploaded in step one) which the client will sign JWTs with.

When adding a capability that requires authentication for the first time, this option will be presented in a wizard:

Configuring the Use of an Asymmetric Key for Authenticating an OAuth Client

Later, this setting can be modified in an existing client in the General configuration section at the top of the client settings page. With this configured, the client will be able to authenticate to any endpoint where authentication is required. Also, be aware that the client will not be able to authenticate using a client ID and secret; only JWTs will be accepted.

User Authentication using JWTs

The second use case that RFC 7523 addresses is user authentication. This scenario is not covered by the OpenID Connect profile, so requests that include scope values defined by that protocol (e.g., profile, email, etc.) are undefined. In Curity's implementation, requesting such scope values will result in a bad request error. Unlike client authentication, which can occur at various concepts, user authentication only takes place at the token endpoint. For these reasons, the protocol is a bit easier to understand than the previous one.

When following this part of the RFC, user authentication and consent are obtained by some other, undefined means. In such a case, the OAuth server receives a JWT asserting who the user is. As long as the OAuth server trusts the issuer of the JWT, it will respond with an access token. The reply will not, however, include a refresh token nor an ID token. The reason that the ID token will not be included is because OpenID Connect, which is where ID tokens are defined, is not in use. The refresh token is not needed either because the client is presumably able to send a new JWT whenever the access token expires. In effect, a client's ability to extend access is controlled by the entity that issues the user JWT grant. The implication is that the party asserting the JWTs must be very trustworthy and capable. Without such an entity, this flow should not be used.

When it is, the request sent to the token endpoint will look something like this:

http
1234567
POST /oauth/v2/oauth-token HTTP/1.1
Host: localhost:8443
Authorization: Basic Y2xpZW50LW9uZTowbmUhU2VjcmV0
Content-Type: application/x-www-form-urlencoded
 
grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer&
assertion=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo4NDQzL2Rldi9vYXV0aC9hbm9ueW1vdXMiLCJpc3MiOiJteS1nb29kLWlzc3VlciIsInN1YiI6ImpvaG5kb2VAZXhhbXBsZS5jb20iLCJleHAiOjE1MzU5NzA0MjgsImp0aSI6IjEyMzQ1NiJ9.M_L1-9YsKq0fOMNl3ECk8_oSfl1cJcJ3mUIk2E2WJ0SYvMD2XO79RqC8TWZ6ikNgkU5dhahBHqrYTSeb6PnMJKcfwjNg0ZEfTegLVBwfADi3UWi-VPnIiolOCfJyh59qkbkl9qBkH1f8sw0m-fXtq8m5LXrwmIamavmY5GicQkwAK38A0eOhMzvBcvHSxcH1_zU8nvZZOnJNhv_ifQr_twrJFZWglo8a5qn0yUcJlDJmM_SC_vGqgV1YkDzpyeniVdsyGA4WFTcmOCYR_iCDwrBnumyYtJd_CWaG_l8d3cT_K4ZR1gRfTM9Pe0cPTCJxnLO-r6JmJ0FVubHmxtoHug

As you can see from this, the client includes the following parameters:

Parameter NameParameter Value
grant_typeurn:ietf:params:oauth:grant-type:jwt-bearer
assertionA JWT that represents an end user

These are similar, but different, from the parameters shown in the table above for client authentication (allowing them to be used together).

Authenticating the User

In a comparable manner, the OAuth server validates the JWT provided in the assertion request parameter. For it to be accepted, it must have a subject (sub) claim; however, the value will be accepted verbatim if the issuer is trusted. To determine this, the JWT must also have an issuer (iss) claim and its value must be the entity ID of the asserting party (i.e., the "identity provider"). Which entity a client trusts is configured for each one in Curity. When enabling the capability for a client, the issuer's ID and public key must be configured:

Configuring the Allowance of the JWT Assertion Grant Type and the Trusted Issuer in the Admin UI

The entity ID can be any value; it is often a URL but need not be. The public key is uploaded in a similar manner as for verifying JWTs issued by the client. The XML configuration that corresponds to the screenshot above is shown in the following listing:

xml
1234567891011121314
<client>
<capabilities>
<assertion>
<jwt>
<trust>
<issuer>my-good-issuer</issuer>
<asymmetric-signing-key>my-good-issuer-public-key</asymmetric-signing-key>
</trust>
<allow-reuse>true</allow-reuse>
</jwt>
</assertion>
</capabilities>
<!-- ... -->
</client>

Besides the verification of the signature and issuer, other claims in the JWT must also be verified. These checks are like JWTs used for client authentication. In particular, the JWT must contain an audience (aud) claim. The value must be the OAuth profile's token endpoint. The JWT must have an expiration time, indicated by the exp claim, which must be in the future; otherwise, the token will be expired and consequently rejected. It may also have a not before (nbf) and issued at (iat) claim. Neither of these are required, but, if present, they will be validated. Lastly, the JWT must have an ID (jti) claim. The uniqueness requirement of these IDs can be configured per client. It is recommended to ensure that the JWTs are unique and contain all of these claims.

Conclusion

Various OAuth-related standards define how JWTs can be used to authenticate users and clients. This allows for new use cases and security postures to be created. Instead of sending a shared secret over the wire, for example, public/private key cryptography can be used to limit access to sensitive key material. The authenticity of users and client apps can still be determined, however, and these apps can obtain access tokens. Thus, API security remains uniform and token-based. These standards also help in conforming to various regulations, including PSD2. Legacy federation services and Identity Providers can also be utilized as a Security Token Issuer (STS). Tokens from such services can be sent to the OAuth server to reuse existing authentication capabilities and Public Key Infrastructure (PKI).

Curity has had support for authenticating clients using JWTs since version 3.0. User authentication support using JWTs came later in 3.1. If you would like to learn more about these capabilities, give them a try. If you run into any problems or have any questions, do not hesitate to contact us.

Join our Newsletter

Get the latest on identity management, API Security and authentication straight to your inbox.

Start Free Trial

Try the Curity Identity Server for Free. Get up and running in 10 minutes.

Start Free Trial