JSON Web Tokens (JWTs) provide a secure and reliable method for user authentication in web applications. This article explores the structure, uses, and advantages of JWTs, along with an example of how to verify their signatures.
Structure of a JWT
A JWT is comprised of 3 base64 encoded sections separated by full stops:
The Header Contains information about the token type (i.e., JWT) and the signing algorithm used (such as HMAC SHA256 or RSA).
The Payload The heart of the JWT, this section stores the actual claims as key-value pairs.
The Verification Signature This verifies the JWT. The signature is calculated using the header, payload, and a secret key or a private/public key pair. Only the holder of the secret or private key can generate a valid signature.
JWT Authentication
JWT Authentication is a triparty trust, the three actors are the client, server, and authority. The client acquires a token from the authority, this token is then sent with every request to the server, the server verifies this token with the authority.
Advantages
- Compact: JWTs are smaller in size than other token formats, making them ideal for transmission over the web.
- Self-contained: JWTs include all necessary information for authentication, reducing the need for server-side session storage.
- Secure: Digital signatures protect the data within JWTs from tampering and ensure source authenticity.
- Stateless: Servers can validate JWTs without maintaining session data, making them suitable for scalable applications.
Important Considerations
- Sensitive Data: Avoid including highly sensitive data in the JWT payload. Tokens may be visible in transit.
- Expiration: Set appropriate expiration times for JWTs to improve security.
- Storage: Properly store JWTs on the client-side to minimize the risk of theft (consider using secure browser storage mechanisms).
Obtaining verification information for a JWT
Let’s Generate a JWT from the Duende Demo website using the Client Credentials Flow
curl --location 'https://demo.duendesoftware.com/connect/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_secret=secret' \
--data-urlencode 'client_id=m2m' \
--data-urlencode 'grant_type=client_credentials'
The Token is as follows:
eyJhbGciOiJSUzI1NiIsImtpZCI6IkZBRDQ3M0VERkRDMzQzOTM4MkFEODAxRDBDQjY0OTNFIiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2RlbW8uZHVlbmRlc29mdHdhcmUuY29tIiwibmJmIjoxNzExNDg3MjcwLCJpYXQiOjE3MTE0ODcyNzAsImV4cCI6MTcxMTQ5MDg3MCwiYXVkIjpbImFwaSIsInVybjpyZXNvdXJjZTEiLCJ1cm46cmVzb3VyY2UyIl0sInNjb3BlIjpbImFwaSIsInJlc291cmNlMS5zY29wZTEiLCJyZXNvdXJjZTEuc2NvcGUyIiwicmVzb3VyY2UyLnNjb3BlMSIsInJlc291cmNlMi5zY29wZTIiLCJyZXNvdXJjZTMuc2NvcGUxIiwicmVzb3VyY2UzLnNjb3BlMiIsInNjb3BlMyIsInNjb3BlNCIsInNoYXJlZC5zY29wZSIsInRyYW5zYWN0aW9uIl0sImNsaWVudF9pZCI6Im0ybSIsImp0aSI6IkEzRUUyNUZCMUIzNzA0RTI4N0FFREY4Njg5OEQ3QTY0In0.iJ2ZsOvjWVaKUupvUkTPdRr20tk0ysnILq6AofikQj6ebZ0OOGmBLrQypc7S_yF_OIs63kf53tw6wU_D4QSavWqhs8P-QlPw0Dv4NOuuFpoQfLg0lVKQytvfn2J-VSP42zIZbR_H3wyThY5-5n0LqbHp6rKLekFSVm4YWmECBCltyToeMgQabyUNAdBlNqRlsSovPC1jaY9muxGnePKBT7msOHB3E3UfsY1iLtB_XcWKneKtwfrNZAKa9CRQj1u-c3H4IhD9G2lPm889i6T_202NgPKyW8Ad0KcWpfIGiELZWUeK2CfluP2sBVRXCmsXIlR7fAp_gy0mMqSfeQnRsQ
if we Base 64 Decode the Header (using Base64 -d)
{
"alg": "RS256",
"kid": "FAD473EDFDC3439382AD801D0CB6493E",
"typ": "at+jwt"
}
- The Algorithm (alg) the Token has been signed by is
RS265
- The Key it has been signed with (kid) is
FAD473EDFDC3439382AD801D0CB6493E
- The Type of token is (typ) is
at+jwt
signifying this is aapplication/at+jwt
or JWT Access Token
The Body contains the following:
{
"iss": "https://demo.duendesoftware.com",
"nbf": 1711487270,
"iat": 1711487270,
"exp": 1711490870,
"aud": [
"api",
"urn:resource1",
"urn:resource2"
],
"scope": [
"api",
"resource1.scope1",
"resource1.scope2",
"resource2.scope1",
"resource2.scope2",
"resource3.scope1",
"resource3.scope2",
"scope3",
"scope4",
"shared.scope",
"transaction"
],
"client_id": "m2m",
"jti": "A3EE25FB1B3704E287AEDF86898D7A64"
}
- The issuer
iss
ishttps://demo.duendesoftware.com
- The Not Valid Before
nbf
is1711487270
(Tue Mar 26 2024 21:07:50 GMT+0000 (Greenwich Mean Time)). - The Issued At
iat
is1711487270
(Tue Mar 26 2024 21:07:50 GMT+0000 (Greenwich Mean Time)). - The Expiry time
exp
is1711490870
(Tue Mar 26 2024 22:07:50 GMT+0000 (Greenwich Mean Time)). - The Audience
aud
is who the token is intended for. - The JWT ID
jti
is the unique identifier for this token.
A good resource for evaluating a JWT is JWT please never paste a JWT anywhere that you do not 100% trust, if the JWT is stolen it can grant access for your accounts to malicious users.
Using this information we can go to the well known Endpoints (defined as part of OpenID Connect) https://demo.duendesoftware.com/.well-known/openid-configuration
to find the jwks_uri
(JSON Web keys Set)
The jwks found at https://demo.duendesoftware.com/.well-known/openid-configuration/jwks
{
"keys": [
{
"kty": "RSA",
"use": "sig",
"kid": "FAD473EDFDC3439382AD801D0CB6493E",
"e": "AQAB",
"n": "zafehxJo6CN9dRmQTZHYy_-4Rk-CEwEgQfY5lgfQbkyS2WmFJNPLVViOZMSnAWkJcYiCcyL9h5z23dsLNhrpoDzu96OWZIGgy0Z1PoWlZSDINQkDN4FAnvPX-QMP7ssVpK7RvNeClRB0O4kkQuIDg3e5u3nqDkwyNgMVRgYVWBqYpq6tpXlTnsWF-xudQFr1Vs8duMU3snIVDxx3GGS0_xCZStQ0KTdtCpjQjZQORHeacMIVkx42RgnDr0ZpvwYBk0sGY4bWJrZQiDhvvH2lUhagt4euGMNLu9qDkL-LL_nx5vdd4rVNZGyk6HuXwyNgIHrQWe4ENN3fp9-kH9aggQ",
"alg": "RS256"
}
]
}
Shows there is one key with a matching kid
to my token above, we can also see that the modulus n
and exponent e
are present for use in verifying the signature.
Code example of verifying a signature
An example of verifying a JWT using C# (The following is done for demonstration purposes, I would recommend using a pre-existing library and not implementing this yourself)
using System.Security.Cryptography;
using System.Text;
var jwt = "eyJhbGciOiJSUzI1NiIsImtpZCI6IkZBRDQ3M0VERkRDMzQzOTM4MkFEODAxRDBDQjY0OTNFIiwidHlwIjoiYXQrand0In0.eyJpc3MiOiJodHRwczovL2RlbW8uZHVlbmRlc29mdHdhcmUuY29tIiwibmJmIjoxNzExNDg3MjcwLCJpYXQiOjE3MTE0ODcyNzAsImV4cCI6MTcxMTQ5MDg3MCwiYXVkIjpbImFwaSIsInVybjpyZXNvdXJjZTEiLCJ1cm46cmVzb3VyY2UyIl0sInNjb3BlIjpbImFwaSIsInJlc291cmNlMS5zY29wZTEiLCJyZXNvdXJjZTEuc2NvcGUyIiwicmVzb3VyY2UyLnNjb3BlMSIsInJlc291cmNlMi5zY29wZTIiLCJyZXNvdXJjZTMuc2NvcGUxIiwicmVzb3VyY2UzLnNjb3BlMiIsInNjb3BlMyIsInNjb3BlNCIsInNoYXJlZC5zY29wZSIsInRyYW5zYWN0aW9uIl0sImNsaWVudF9pZCI6Im0ybSIsImp0aSI6IkEzRUUyNUZCMUIzNzA0RTI4N0FFREY4Njg5OEQ3QTY0In0.iJ2ZsOvjWVaKUupvUkTPdRr20tk0ysnILq6AofikQj6ebZ0OOGmBLrQypc7S_yF_OIs63kf53tw6wU_D4QSavWqhs8P-QlPw0Dv4NOuuFpoQfLg0lVKQytvfn2J-VSP42zIZbR_H3wyThY5-5n0LqbHp6rKLekFSVm4YWmECBCltyToeMgQabyUNAdBlNqRlsSovPC1jaY9muxGnePKBT7msOHB3E3UfsY1iLtB_XcWKneKtwfrNZAKa9CRQj1u-c3H4IhD9G2lPm889i6T_202NgPKyW8Ad0KcWpfIGiELZWUeK2CfluP2sBVRXCmsXIlR7fAp_gy0mMqSfeQnRsQ";
var jwtParts = jwt.Split('.');
var header = jwtParts[0];
var payload = jwtParts[1];
var signature = jwtParts[2];
var publicKey = new RSAParameters()
{
Exponent = FromBase64("AQAB"), // Exponent from jwks
//Modulus from jwks
Modulus = FromBase64("zafehxJo6CN9dRmQTZHYy_-4Rk-CEwEgQfY5lgfQbkyS2WmFJNPLVViOZMSnAWkJcYiCcyL9h5z23dsLNhrpoDzu96OWZIGgy0Z1PoWlZSDINQkDN4FAnvPX-QMP7ssVpK7RvNeClRB0O4kkQuIDg3e5u3nqDkwyNgMVRgYVWBqYpq6tpXlTnsWF-xudQFr1Vs8duMU3snIVDxx3GGS0_xCZStQ0KTdtCpjQjZQORHeacMIVkx42RgnDr0ZpvwYBk0sGY4bWJrZQiDhvvH2lUhagt4euGMNLu9qDkL-LL_nx5vdd4rVNZGyk6HuXwyNgIHrQWe4ENN3fp9-kH9aggQ")
};
bool success = false;
using (var rsa = new RSACryptoServiceProvider())
{
rsa.ImportParameters(publicKey);
SHA256 sha256 = SHA256.Create();
var encoder = new UTF8Encoding();
byte[] hash = sha256.ComputeHash(encoder.GetBytes($"{header}.{payload}"));
RSAPKCS1SignatureDeformatter rsaDeformatter = new RSAPKCS1SignatureDeformatter(rsa);
rsaDeformatter.SetHashAlgorithm("SHA256"); //using algorithm for RS265
success = rsaDeformatter.VerifySignature(hash,
FromBase64(signature));
}
// Ensure Padding is correct, Replace invalid Characters, and convert to Byte Array
Byte[] FromBase64(string base64String)
{
var paddingChars = base64String.Length % 4;
if (paddingChars == 3)
paddingChars = 1;
return Convert.FromBase64String(base64String.PadRight(base64String.Length + paddingChars, '=').Replace("_", "/")
.Replace("-", "+"));
}
Conclusion
JWTs provide a robust and standardized method for secure authentication within web applications and APIs. Their compact size, self-contained nature, and digital signatures make them a compelling choice for modern developers focused on security and scalability. Always remember to prioritize responsible implementation, including secure storage, expiration settings, and using well-established libraries.