JSON Web Tokens (JWTs) and Signing Keys

JSON Web Tokens (JWTs) are a compact, self-contained way to represent information between two parties as a JSON object. In Wristband, JWTs are used authentication. The JWTs consist of three parts: a header, a payload, and a signature.

JWT Header

The header is the first part of a JWT and is a base64Url-encoded JSON object. It consists of three parts:

  • kid (Key ID): Identifies the key used to sign the token. This is important for working with JSON Web Key Sets (JWKS) for key management and rotation. It allows for the easy lookup of the correct key for signature verification.

  • alg (Algorithm): Specifies the cryptographic algorithm used to sign the token. This is important for both creating and verifying tokens and ensures that the recipient knows which algorithm to use for signature validation. Wristband supports the RS256 algorithm (RSA with SHA-256) for access tokens and ID tokens. For refresh tokens, Wristband supports the HS256 algorithm.

  • typ (Type): Defines the type of the token as outlined in RFC 9068. It allows for clear identification of the token's purpose and expected usage, and it ensures that the token is treated appropriately by the recipient. Wristband supports the following token types:

    "typ" ValueToken Type
    at+JWTAccess Token
    id+JWTID Token
    rt+JWTRefresh Token

Here's an example of a JWT header for a Wristband access token:

{
  "kid": "r44ow3paz5crpm5dgvuaerhs2a",
  "typ": "at+JWT",
  "alg": "RS256"
}

JWT Payload

The payload is the second part of a JWT, and it is also a base64Url-encoded JSON object. It contains the claims or statements about the entity including who the subject of the token is, their scope of access, and any other contextual data. The payload content is dynamic and changes depending on the type of token and the subject.

Access Token Payload

Access tokens are short-lived security tokens that grant users and machines permission to access specific resources or perform actions in your application. There are variations of claims that can be found in the payload of access tokens depending on the subject kind. The following subject kinds are supported for access tokens:

Subject KindDescription
userA human user
application_clientA machine-to-machine OAuth client owned at the application level
tenant_clientA machine-to-machine OAuth client owned at the tenant level
User Subject Kind

Here's an example of a payload for an access token where the subject kind is a user:

{
  "sub": "dgduqyd42veeffcex6nulzfpba",
  "idp_name": "wristband",
  "van_dom": "auth.yourapp.io",
  "amr": [
    "pwd"
  ],
  "iss": "https://auth.yourapp.io.com",
  "client_id": "nfqsd5qs4jflzkmhe5ambkieky",
  "tnt_id": "ephrgbr36zgbrplbwi7dkqweui",
  "scope": "openid offline_access",
  "auth_time": 1697517244,
  "is_root_app": true,
  "sub_kind": "user",
  "exp": 1697519044,
  "app_id": "m7k55onh6nbsjggayhubbef4eq",
  "iat": 1697517244,
  "jti": "alltndyuhfcmdbf4ow7zuzs6hq",
  "custom_claims": {
    "claim_a": "foo",
    "claim_b": "bar"
  }
}

These are the access token claims for human users that authenticate with Wristband:

ClaimFormatDescription
amrarray of stringsList of authentication method references indicating how the user authenticated. Currently supported methods include:
  • email_link: An email link was used to authenticate the user (e.g. magic link, activation email, etc.)
  • external_idp: An external identity provider was used to authenticate the user.
  • pwd: A password was used to authenticate the user.
app_idstringThe UUID of the application associated with the access token. For a user subject, this is the UUID of the application that the user belongs to.
auth_timenumberTime at which the user authenticated, represented as the number of seconds from the epoch that have elapsed.
client_idstringThe UUID of the OAuth client that was used to create the token.
custom_claimsobjectAn object containing configured custom claims.
expnumberThe time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed.
iatnumberThe time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed.
idp_namestringThe name of the identity provider that the user belongs to.
issstringAn application vanity URL in the format https://<application_vanity_domain>. The <application_vanity_domain> will be the vanity domain of the application that the user belongs to.
jtistringA unique identifier for the token.
scopestringA JSON string containing a space-separated list of OIDC scopes associated with this token. These scopes define what claims will be returned in the userinfo API response.
substringThe UUID of the subject that the access token was issued on behalf of. For a user subject, this claim represents their userId in Wristband.
sub_kindstringThe kind of subject the token is for. For a user subject, the value is user.
tnt_idstringThe UUID of the tenant associated with the access token. For a user subject, this is the UUID of the tenant that the user belongs to.
van_domstringThe vanity domain that was used when calling the token endpoint. For a client associated with an application, this should be the application's vanity domain. For a client associated with a tenant, this should be the application's vanity domain that the tenant belongs to.
Application Client Subject Kind

Here's an example of a payload for an access token where the subject kind is an application-level machine-to-machine OAuth client:

{
  "sub": "cdgqsdk3mrbfvidlrotm2a4eiu",
  "van_dom": "auth.yourapp.io",
  "iss": "https://auth.yourapp.io",
  "sub_kind": "application_client",
  "exp": 1697674367,
  "app_id": "4satxbaxb5fjncfsqzngnwwswi",
  "iat": 1697587967,
  "jti": "bjgjhrf5jregdjxjcxp4i7ckfe",
  "client_id": "cdgqsdk3mrbfvidlrotm2a4eiu",
  "custom_claims": {
    "claim_a": "foo",
    "claim_b": "bar"
  }
}

These are the access token claims for application-level machine-to-machine OAuth clients that authenticate with Wristband:

ClaimFormatDescription
app_idstringThe UUID of the application associated with the access token. For a client subject that is owned by an application, this will be the application that owns the client.
client_idstringThe UUID of the OAuth client that was used to create the token.
custom_claimsobjectAn object containing configured custom claims.
expnumberThe time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed.
iatnumberThe time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed.
issstringAn application vanity URL in the format https://<application_vanity_domain>. The <application_vanity_domain> will be the vanity domain of the application that the client belongs to.
jtistringA unique identifier for the token.
substringThe UUID of the subject that the access token was issued on behalf of. For an application-level OAuth client subject, this claim will be the same as client_id.
sub_kindstringThe kind of subject the token is for. For an application-level OAuth client subject, the value is application_client.
van_domstringThe vanity domain that was used when calling the token endpoint. For a client associated with an application, this should be the application's vanity domain.
Tenant Client Subject Kind

Here's an example of a payload for an access token where the subject kind is a tenant-level machine-to-machine OAuth client:

{
  "sub": "h3cdthdxordi3n2jlcobz7mvw4",
  "tnt_id": "rztradojyfc47f74d3fh5fylre",
  "van_dom": "auth.yourapp.io",
  "iss": "https://auth.yourapp.io",
  "sub_kind": "tenant_client",
  "exp": 1697677760,
  "app_id": "4satxbaxb5fjncfsqzngnwwswi",
  "iat": 1697591360,
  "jti": "54eoa4aiybe5zgqdf67jskdyue",
  "client_id": "h3cdthdxordi3n2jlcobz7mvw4",
  "custom_claims": {
    "claim_a": "foo",
    "claim_b": "bar"
  }
}

These are the access token claims for tenant-level machine-to-machine OAuth clients that authenticate with Wristband:

ClaimFormatDescription
app_idstringThe UUID of the application associated with the access token. For a client subject owned by a tenant, this will be the application that the tenant belongs to.
client_idstringThe UUID of the OAuth client that was used to create the token.
custom_claimsobjectAn object containing configured custom claims.
expnumberThe time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed.
iatnumberThe time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed.
issstringAn application vanity URL in the format https://<application_vanity_domain>. The <application_vanity_domain> will be the vanity domain of the application that the client belongs to.
jtistringA unique identifier for the token.
substringThe UUID of the subject that the access token was issued on behalf of. For a tenant-level OAuth client subject, this claim will be the same as client_id.
sub_kindstringThe kind of subject the token is for. For a tenant-level OAuth client subject, the value is tenant_client.
tnt_idstringThe UUID of the tenant associated with the access token. For a client subject owned by a tenant, this will be the UUID of the owning tenant.
van_domstringThe vanity domain that was used when calling the token endpoint. For a client associated to a tenant, this should be the application's vanity domain that the tenant belongs to.

ID Token Payload

ID tokens serve to confirm a user's identity during the authentication process and provide applications with essential user information, allowing them to personalize the user's experience and make access control decisions while maintaining security and privacy.

Here's an example of a payload for an ID token for an authentication user subject:

{
  "at_hash": "IhchIEvr_qd35VVhW2V3Xg",
  "sub": "dgduqyd42veeffcex6nulzfpba",
  "idp_name": "wristband",
  "amr": [
    "pwd"
  ],
  "iss": "https://yourapp-yourcompany.us.wristband.dev",
  "auth_flow": "authorization_code",
  "nonce": "jPy_qLTHFbT1kisduhYgXREVvpY9RPRk97YSywHpk_A",
  "sid": "4glkn7jfqzbdvfycwa6uwhfmry",
  "rt_hash": "xhI6JZLwpvfxltdjtseyHQ",
  "aud": "nfqsd5qs4jflzkmhe5ambkieky",
  "tnt_id": "ephrgbr36zgbrplbwi7dkqweui",
  "auth_time": 1697587898,
  "exp": 1697589698,
  "app_id": "m7k55onh6nbsjggayhubbef4eq",
  "iat": 1697587898,
  "jti": "gzqma35o4nbrnnjrujlpdsxknu",
  "custom_claims": {
    "claim_a": "foo",
    "claim_b": "bar"
  }
}

These are the ID token claims for human users that authenticate with Wristband:

ClaimFormatDescription
amrarray of stringsList of authentication method references indicating how the user authenticated. Currently supported methods include:
  • email_link: An email link was used to authenticate the user (e.g. magic link, activation email, etc.)
  • external_idp: An external identity provider was used to authenticate the user.
  • pwd: A password was used to authenticate the user.
app_idstringThe UUID of the application that the user belongs to.
at_hashstringThe hash of the access token. An application can calculate the at_hash from its access token and compare it to the one in the ID token to ensure token authenticity and integrity.
audstringThe OIDC spec allows for the aud claim format to be either a string or an array of strings. In Wristband, the aud value will always be a string, and the value should always be equal to the client_id value in the access token for the OAuth client that initiated the authorization request.
auth_flowstringRepresents which authentication flow was used to produce the ID token. Currently supported flows include:
  • authorization_code: The OAuth2 authorization code flow.
  • refresh_token: The OAuth2 refresh token flow.
auth_timenumberTime at which the user authenticated, represented as the number of seconds from the epoch that have elapsed.
custom_claimsobjectAn object containing configured custom claims.
expnumberThe time at which the ID token should expire, represented as the number of seconds from the epoch that have elapsed.
iatnumberThe time at which the ID token was issued, represented as the number of seconds from the epoch that have elapsed.
idp_namestringThe name of the identity provider that the user belongs to.
issstringAn application vanity URL in the format https://<application_vanity_domain>. The <application_vanity_domain> will be the vanity domain of the application that the user belongs to.
jtistringA unique identifier for the token.
noncestringThe nonce value that was passed in the original authorization request and can be used for verification.
rt_hashstringThe hash of the refresh token. An application can calculate the rt_hash from its refresh token and compare it to the one in the ID token to ensure token authenticity and integrity.
sidstringThe UUID of the auth session associated with the ID token.
substringThe UUID of the authenticated user.
tnt_idstringThe UUID of the tenant that the user belongs to.

Refresh Tokens

Refresh tokens in Wristband are effectively opaque. This means that you should not rely on the contents of the refresh token since it only has meaning to the Wristband platform internally. You can simply send the refresh token to Wristband for validation and exchanging for new access tokens.

JWT Signature

The JWT signature is a cryptographic stamp of authenticity applied to the header and payload of a JWT. At Wristband, we currently support the RS256 algorithm for generating this signature. RS256 utilizes an asymmetric key pair, consisting of a private key for signing and a public key for verification. The signature ensures the integrity and authenticity of the token, preventing unauthorized tampering or modification. This is vital for verifying that the JWT hasn't been altered during transmission or by unauthorized parties.

JSON Web Key Set (JWKS)

JSON Web Key Set (JWKS) is a standardized format for representing a collection of public keys used for verifying JWTs. As mentioned above, there is a pair of public and private keys used for verifying and signing any Wristband JWTs, respectively. Wristband supports JWKS endpoints to allow your application a means for obtaining the public keys necessary to verify the integrity of JWTs.

Signing Key Granularity

At Wristband, we offer an exceptionally fine-grained scope for associating signing keys. The narrower the operational scope of these keys, the less impact a security breach can have. This is critical for minimizing the potential consequences of a security breach, safeguarding against wide-reaching effects in the event of a compromise of private signing keys.

In an ideal scenario, customers would expect a vendor like Wristband to give each customer their own unique signing keys that are separate from every other Wristband customer. We actually take that one step further! Each application in Wristband for just a single customer has its own unique signing keys.

Signing Keys Model

Having per-application signing keys in Wristband means that, in the incredibly rare event that your application's signing keys were ever compromised in a breach, the blast radius would be minimal and self-contained to just that particular application. For example, let's imagine a fictional scenario where Customer X builds two applications on the Wristband platform: Application A and Application B. For theoretical purposes, let us say that the signing keys for Application A were compromised.

Signing Key Breach Example

In this scenario, the blast radius from such a breach can be qualified by the following:

  • No other customers of Wristband will be affected by Application A signing keys being compromised.
  • Application B, which belongs to Customer X, will not be affected by Application A signing keys being compromised.

Customers of Wristband can generate new signing keys and rotate out old signing keys at any time (more on that below).

Working with JWTs and Signing Keys

There are several operations that can be performed when it comes to token and key management.

Verifying Access and ID Tokens

When verifying the integrity of any access or ID token, your application should validate both the payload and signature. For the payload, the two most important claims to verify are the iss (issuer) and exp (expiration time) claims.

Issuer Verification (iss):

Verifying the issuer claim of a token is important for confirming that the tokens came from the expected and trusted issuer. This prevents acceptance of tokens from unauthorized or malicious sources and ensures that the token is issued by a legitimate identity provider. It safeguards against attackers trying to inject their own tokens into the system.

Your application should simply ensure the value is equal to a value you expect. In Wristband, the issuer can be:

  • An application vanity domain that is generated by Wristband if custom domains are not active for your application; i.e.: https://yourapp-yourcompany.us.wristband.dev
  • A domain of your choosing if custom domains are active for your application; i.e.: https://auth.yourapp.io

Expiration Time Verification (exp):

Your application should check that the access token is valid at the time of use as dictated by the expiration time claim. Any token that is past the expiration time is considered invalid and can not be used anymore. Checking the expiration time reduces the window of opportunity for unauthorized access.

Verifying JWT Signatures

Verifying the signature of a JWT is essential because it ensures data integrity, authenticates the token's source, prevents token tampering, and guards against unauthorized access and token substitution. There are two ways you can validate a JWT.

Verifying with the Introspection API

Wristband exposes a Introspect Token API that you can call from your application's code. You can pass the token to Wristband that you want to have verified to the Introspection API. Wristband will then return a response that tells you if the token is valid or not. If it is valid, it will return token claims in the response in addition to its status.

📘

Note About The Introspection API

The Introspection API will not only validate the signature of a token, but it also validates the claims in the payload.

There is a performance tradeoff to using the Token Introspection API. While using the API can offload some of the application logic to Wristband, it increases the volume of network activity since your application would have to make API calls frequently to perform the verification.

Verifying with Signing Keys

Alternatively, you can use public keys that you acquire from Wristband's JWKS API. This is the preferred method for verifying token signatures. You can call to the API once when your application starts up and then proceed to cache the public keys somewhere in your application. Then, every time you need to verify a token, you can pull the keys out of the cache to make the verification and avoid any network interaction with Wristband.

Obtaining New Access Tokens

During token verification, if your access token was valid but merely expired, then you would need to obtain a new access token. There are a couple ways to acquire new tokens:

  • For human users that authenticated through the OAuth2 authorization code flow, a refresh token can be used to exchange for a new access token by calling Wristband's Token API using the authorization_code grant type.
  • For machine-to-machine OAuth clients, the client ID and client secret can be used to exchange for a new access token by calling Wristband's Token API using the client_credentials grant type.

In the event that your token had other invalid claims and/or an invalid signature, then:

  • For human users, they should be redirected to the login page to reauthenticate.
  • For machine-to-machine OAuth clients, the client ID and client secret can be used to exchange for a new access token by calling Wristband's Token API using the client_credentials grant type.

Signing Key Rotation

Signing key rotation is a security practice involving the periodic replacement of the signing keys used for creating and verifying tokens. This reduces the risk of a key being compromised and ensures that even if a key is compromised, its window of usefulness is limited. In Wristband, each application has its own unique set of signing keys that can be rotated at any time.

In order to facilitate key rotation, a signing key can be in one of three positions:

PositionDescription
CurrentThis is the signing key that is actively being used to sign new tokens.
PreviousAfter rotation, the signing key in the current position will be transitioned to the previous position. In this position, it will no longer be used to sign new tokens but can still be used to verify older tokens that were signed with this key.
NextAfter rotation, the signing key in the next position will be transitioned to the current position. The signing key in the next position can be used to pre-load your application with the signing key that will be used after performing a rotation.