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 theRS256
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" Value Token Type at+JWT Access Token id+JWT ID Token rt+JWT Refresh 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 Kind | Description |
---|---|
user | A human user |
application_client | A machine-to-machine OAuth client owned at the application level |
tenant_client | A 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:
Claim | Format | Description |
---|---|---|
amr | array of strings | List of authentication method references indicating how the user authenticated. Currently supported methods include:
|
app_id | string | The 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_time | number | Time at which the user authenticated, represented as the number of seconds from the epoch that have elapsed. |
client_id | string | The UUID of the OAuth client that was used to create the token. |
custom_claims | object | An object containing configured custom claims. |
exp | number | The time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed. |
iat | number | The time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed. |
idp_name | string | The name of the identity provider that the user belongs to. |
iss | string | An 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. |
jti | string | A unique identifier for the token. |
scope | string | A 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. |
sub | string | The 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_kind | string | The kind of subject the token is for. For a user subject, the value is user . |
tnt_id | string | The 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_dom | string | The 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:
Claim | Format | Description |
---|---|---|
app_id | string | The 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_id | string | The UUID of the OAuth client that was used to create the token. |
custom_claims | object | An object containing configured custom claims. |
exp | number | The time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed. |
iat | number | The time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed. |
iss | string | An 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. |
jti | string | A unique identifier for the token. |
sub | string | The 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_kind | string | The kind of subject the token is for. For an application-level OAuth client subject, the value is application_client . |
van_dom | string | The 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:
Claim | Format | Description |
---|---|---|
app_id | string | The 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_id | string | The UUID of the OAuth client that was used to create the token. |
custom_claims | object | An object containing configured custom claims. |
exp | number | The time at which the access token should expire, represented as the number of seconds from the epoch that have elapsed. |
iat | number | The time at which the access token was issued, represented as the number of seconds from the epoch that have elapsed. |
iss | string | An 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. |
jti | string | A unique identifier for the token. |
sub | string | The 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_kind | string | The kind of subject the token is for. For a tenant-level OAuth client subject, the value is tenant_client . |
tnt_id | string | The 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_dom | string | The 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:
Claim | Format | Description |
---|---|---|
amr | array of strings | List of authentication method references indicating how the user authenticated. Currently supported methods include:
|
app_id | string | The UUID of the application that the user belongs to. |
at_hash | string | The 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. |
aud | string | The 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_flow | string | Represents which authentication flow was used to produce the ID token. Currently supported flows include:
|
auth_time | number | Time at which the user authenticated, represented as the number of seconds from the epoch that have elapsed. |
custom_claims | object | An object containing configured custom claims. |
exp | number | The time at which the ID token should expire, represented as the number of seconds from the epoch that have elapsed. |
iat | number | The time at which the ID token was issued, represented as the number of seconds from the epoch that have elapsed. |
idp_name | string | The name of the identity provider that the user belongs to. |
iss | string | An 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. |
jti | string | A unique identifier for the token. |
nonce | string | The nonce value that was passed in the original authorization request and can be used for verification. |
rt_hash | string | The 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. |
sid | string | The UUID of the auth session associated with the ID token. |
sub | string | The UUID of the authenticated user. |
tnt_id | string | The 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.
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.
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
):
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
):
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:
Position | Description |
---|---|
Current | This is the signing key that is actively being used to sign new tokens. |
Previous | After 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. |
Next | After 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. |
Updated 12 days ago