JWT - Java, Security Lesson
Period 1 - Emma, Vivian, Grace, Aliya, Kevin, Isabelle
What is JWT
- JSON Web Token (JWT)
- popular way to authenticate users in a web application. It is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).
- Compact, url safe means of representing claims to securely transmit information between parties.
- These claims are encoded as JSON objects that are digitally signed using a JSON web signature
- Concise and efficient representation, allows for ease of transmissions over networks
- No need to reference external source/database to validate info
- Common use cases:
- Authentication
- information exchange
- Authorization
- Secure your APIs!
Popcorn hack: list 3 real world applications of JWT: banks, amazon shopping, accounts online
Why do you need JWT
JSON Web Tokens (JWT) are crucial for secure and efficient user authentication in web development.
- They help manage user identity and sessions across different parts of a system.
- They play a key role in stateless authentication, allowing servers to verify user identity without storing session data.
- JWTs are especially useful in decentralized systems, enabling smooth communication between different services and ensuring secure user roles and permissions.
- JWTs simplify and enhance user authentication in modern web development.
Components of JWT
- What is a web token?
- A web token is a piece of information that represents a user’s identity or session and is used for authentication and authorization in web applications. It is typically a string of characters, often encoded in a JSON format, and is digitally signed to ensure its integrity.
- This is the structure of a JSON Web Token:
- Header
- The header typically consists of two parts: the type of the token, which is JWT, and the algorithm that is used, such as HMAC SHA256 or RSA SHA256. It is Base64Url encoded to form the first part of the JWT
- Payload
- Claims and user data
- claims are statements about the entity (users)
- There are three types of claims:
- registered: predefined claims that are no mandatory but recommended
- public: claims defined within the IA and JSON web token registry
- private: custom claims created to share information between parties that agree to using them
- Signature
- Ensuring integrity and authenticity
- verify the sender of the JWT
- function
- Creating a signature
- Verification process
- Signature tampering activity
Deep Dive into Anatomy of JWT
- Navigate to this website: Link
Encoded: Json Web Token (what you send to and from the client) Decoded: algorithm, data, verify token hasn’t been changed
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// header
{
"alg": "HS256", //type of sign in algorithm used for encoding and decoding
"typ": "JWT" //type of token
}
- Base 64 encoded
- useful to signature type to decode
// payload
{
"sub": "123", //example of a registered claim
"name": "jwt lesson",
"iat": 1516239022",
"authorities": [
"ADMIN",
"MANAGER"
],
"extra-claims": "some data here"
}
- where all different data for applciation is
- sub = subject : id of user you’re authenticating
- iat = issued at (when token was created)
- exp/eat = expired at (date when toke becomes invalid)
//signature
{
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
) secret base64 encoded
}
- verify user does not change with token before it is sent back to you
- input your secret key
- uses algorithm in header to encode data (header + payload) using specific secret key
- if header/payload is changed, signature will not match
Header
import java.util.Base64;
public class JwtHeaderExample {
public static void main(String[] args) {
// JWT Header
String algorithm = "HS256";
String type = "JWT";
// Combine Header properties
String header = "{\"alg\":\"" + algorithm + "\",\"typ\":\"" + type + "\"}";
// Base64 URL encode the header
String base64UrlHeader = Base64.getUrlEncoder().encodeToString(header.getBytes());
System.out.println("JWT Header: " + base64UrlHeader);
}
}
// Run this code
JwtHeaderExample.main(null);
JWT Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
public class JwtPayloadExample {
public static void main(String[] args) {
// JWT Payload (Claims)
Map<String, Object> claims = new HashMap<>();
claims.put("name", "Grace");
claims.put("class", "CSA");
claims.put("exp", System.currentTimeMillis() + 1800000); // 30 minutes expiration
// Convert Claims to JSON and Base64 URL encode the payload
String payload = mapToJson(claims);
String base64UrlPayload = Base64.getUrlEncoder().encodeToString(payload.getBytes());
System.out.println("JWT Payload: " + base64UrlPayload);
}
private static String mapToJson(Map<String, Object> map) {
// Implement JSON conversion logic (use your preferred approach)
return "{ \"customKey\": \"customValue\" }";
}
}
// Run This Code
JwtPayloadExample.main(null);
JWT Payload: eyAiY3VzdG9tS2V5IjogImN1c3RvbVZhbHVlIiB9
Signature
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class JwtSignatureExample {
public static void main(String[] args) {
// Secret key for encoding and decoding
String secretKey = "your secret key";
// Combine Header and Payload with a period '.'
String base64UrlHeader = "base64UrlHeader"; // Placeholder for the actual base64UrlHeader
String base64UrlPayload = "base64UrlPayload"; // Placeholder for the actual base64UrlPayload
String headerPayload = base64UrlHeader + "." + base64UrlPayload;
// Generate HMAC SHA-256 signature
byte[] signature = HmacSha256(headerPayload, secretKey);
// Base64 URL encode the signature
String base64UrlSignature = Base64.getUrlEncoder().encodeToString(signature);
System.out.println("JWT Signature: " + base64UrlSignature);
}
private static byte[] HmacSha256(String data, String key) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKey);
return sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error while generating HMAC SHA-256", e);
}
}
}
// Run
JwtSignatureExample.main(null);
JWT Signature: XHbXLpIy4gwj57PYEH4S6pgQROJAQESSXeVag34MNBo=
Putting It Together
Popcorn hack: write some comments that identify each part of the JWT
- header
- payload
- signature
- algorithm
- secret key
etc
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public class JwtGenerator {
public static void main(String[] args) {
// Secret key used for signing the JWT
String secretKey = "yourSecretKey";
// Subject or user ID for whom the token is generated
String subject = "userId123";
// Expiration time for the token (1 hour from the current time)
long expirationTimeMillis = System.currentTimeMillis() + 3600000;
// Build and print the generated JWT
String jwt = buildJwt(secretKey, subject, expirationTimeMillis);
System.out.println("Generated JWT: " + jwt);
}
// Build JWT by combining header, payload, and signature
private static String buildJwt(String secretKey, String subject, long expirationTimeMillis) {
// JWT header specifying the signing algorithm
String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
// JWT payload containing claims such as subject, issued time, and expiration time
String payload = "{\"sub\":\"" + subject + "\",\"iat\":" + System.currentTimeMillis() / 1000 +
",\"exp\":" + expirationTimeMillis / 1000 + "}";
// Encode header and payload separately
String encodedHeader = base64UrlEncode(header);
String encodedPayload = base64UrlEncode(payload);
// Combine encoded header and payload with a dot separator
String dataToSign = encodedHeader + "." + encodedPayload;
// Combine data with the signature and return the final JWT
String signature = signData(dataToSign, secretKey);
System.out.println(signature);
return dataToSign + "." + signature;
}
// Base64 URL encode helper function
private static String base64UrlEncode(String input) {
return Base64.getUrlEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
}
// Sign the data using HMAC SHA-256 algorithm and the secret key
private static String signData(String data, String secretKey) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKeySpec);
byte[] signature = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// Encode the signature and return as a base64 URL-encoded string
return base64UrlEncode(new String(signature, StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("Error signing JWT", e);
}
}
}
JwtGenerator.main(null);
LW7vv70cHUIcBgsj77-9XO-_ve-_vW9zBlvvv71v77-977-9Ku-_vXxjG--_vRfvv73vv713
Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQxMjMiLCJpYXQiOjE3MDMwOTc3MDMsImV4cCI6MTcwMzEwMTMwM30=.LW7vv70cHUIcBgsj77-9XO-_ve-_vW9zBlvvv71v77-977-9Ku-_vXxjG--_vRfvv73vv713
JWT with Spring Boot
Big Idea
- JwtAuthFilter is executed first (validates and checks JWT Tokens)
- Internal check to check if the token is there or not.
- Extracts the subject (username or email)
- If token is missing → 403 error sent
- If token is there:
- Makes a call using the UserDetails Service to fetch user info from the database. Uses the subject extracted to do so
- Response comes back from the database (user either exists or doesn’t)
- If the user does not exist → 403 error sent
- If user exists → validate JWT process is started
- Validate JWT Process
- Calls the jwt service which takes the user and jwt token as parameters
- Token isn’t valid → 403 error sent
- Token is valid → update the security context holder and set the connected user. User is now authenticated yay!
- Automatically dispatch request → sent to dispatcher servlet → sent to controller → allow all execution user wants to do
- Sends http 200
Here is a diagram that makes it easier to understand the process tha happens between JWT and the API calls.
- User posts with their login information
- server does authentication to confirm the user
- store the user in the memory of the server, send a corresponding ID with a cookie back to the client
- Now every time the user sends a request, the cookie with that corresponding ID gets send to the server which verifies the ID and sends back a response.
Popcorn Hack: Create your own diagram to help visualize the JWT process
JWT Walkthrough
- Clone this repo and follow along in each JWT file: https://github.com/vivianknee/jwtlessonBackend.git
Logic flow
Step 1 (Client - Login Request): The client sends a login request with user credentials (username and password) to the /authenticate endpoint.
Step 2 (JwtApiController):
- The JwtApiController receives the login request.
- It authenticates the user credentials using the AuthenticationManager.
- If authentication is successful: Retrieves user details using the PersonDetailsService. Generates a JWT using the JwtTokenUtil. Sends the JWT as an HTTP-only secure cookie in the response.
Step 3 (Client - Subsequent Requests):
- The client includes the JWT cookie in the headers of subsequent requests.
Step 4 (JwtRequestFilter):
- For each incoming request, the JwtRequestFilter intercepts the request.
- Extracts the JWT from the HTTP request headers or cookies.
- Validates the JWT using the JwtTokenUtil.
- If the token is valid, sets up authentication using Spring Security’s SecurityContextHolder.
Step 5 (Spring Security):
- Spring Security processes the request with the authenticated user.
- The application can now authorize the user based on the roles and permissions associated with the JWT.
Step 6 (Error Handling - JwtAuthenticationEntryPoint):
- If the JWT is missing, invalid, or expired, and the request requires authentication, the JwtAuthenticationEntryPoint handles the authentication failure.
- Responds with an HTTP 401 (Unauthorized) status.
Storing JWT
There are a few different options for storing a JWT in a JavaScript application:
-
Cookies: You can store the JWT in a cookie and send it back to the server with each request. This is a simple and widely-supported option, but it has some limitations. For example, you can’t access cookies from JavaScript on a different domain, and some users may have cookies disabled in their browser settings.
-
Local storage: You can store the JWT in the browser’s local storage (localStorage) or session storage (sessionStorage). This option allows you to access the JWT from JavaScript on the same domain, but it is vulnerable to cross-site scripting (XSS) attacks, where an attacker can inject malicious code into your application and steal the JWT from the storage.
-
HttpOnly cookie: You can store the JWT in an HttpOnly cookie, which is a cookie that can only be accessed by the server and not by client-side JavaScript. This option provides some protection against XSS attacks, but it is still vulnerable to other types of attacks, such as cross-site request forgery (CSRF).
Postman
- Trying to access /api/person/ without jwt token stored in Cookies
- Trying to access /api/person/ with a wrong jwt token stored in Cookies
- Trying to access /api/person/ with a jwt token that does not have the signature
- Accessing /api/person/ with generated jwt token stored in Cookies header
Implementation Approaches and Security Considerations
JWTs are signed to ensure they cannot be modified in transit. Signature is a crucial aspect of JWT security.
Key Usage
- Token Issuance: When the token is issued by the authorization server, it is signed with a key.
- Token Reception: When the client receives the token, the signature is validated using the key.
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.Claims;
public class TokenReceptionExample {
// Simulate receiving a token from the client
private static String receiveTokenFromClient() {
// In a real scenario, this would be received from the client (e.g., from a request header)
return "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiAiMTIzIiwgImV4cCI6IDE2MjM5NzYzODF9.4X1lC5fU4dV1n9l02LZyGQSy5K-O5fnZM0t6eO-w2Qs";
}
public static void main(String[] args) {
// Example of Token Reception
String receivedToken = receiveTokenFromClient();
System.out.println("Received Token: " + receivedToken);
}
}
Symmetric vs. Asymmetric Key Approaches
- Symmetric Key: A single secret key is used both to sign and validate the token.
- Asymmetric Key: Different keys are used to sign and validate the token, only the authorization server has the ability to sign it.
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import java.security.Key;
public class SymmetricKeyValidationExample {
// Symmetric Key for Token Signing and Validation
private static Key symmetricKey = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256);
// Token Validation with Symmetric Key
private static Claims validateTokenSymmetric(String token) {
try {
// Parse and verify the token using the symmetric key and the HS256 algorithm
Jws<Claims> claimsJws = Jwts.parserBuilder().setSigningKey(symmetricKey).build().parseClaimsJws(token);
return claimsJws.getBody();
} catch (ExpiredJwtException e) {
// Handle case where the token has expired
System.out.println("Token has expired");
} catch (MalformedJwtException e) {
// Handle case where the token is invalid (e.g., tampered with)
System.out.println("Invalid token");
}
return null;
}
public static void main(String[] args) {
// Example of Token Validation with Symmetric Key
String receivedToken = receiveTokenFromClient();
Claims validatedClaims = validateTokenSymmetric(receivedToken);
if (validatedClaims != null) {
System.out.println("Validated Payload: " + validatedClaims);
}
}
}
Additional Security Considerations
- Token Scope: limit token access to specific resources or actions
- Token Revocation: implement mechanism to revoke token if user’s access needs to be taken away
- Token Encryption: encrypt token if it contains sensitive information that shouldn’t be visible (even if intercepted)
- Regular Key Rotation: rotate keys regularly for security and to limit the impact of compromised key
Hacks
- Implementation
- Implement a simple authentication system using JWT with Java. Show how JWT works with postman. Get the request to be authorized
- MC Knowledge test (5)
- Which part of the JWT contains the actual data (claims)?
- Header
- Payload: this one
- Signature
- Encryption
- What is the purpose of the header in a JWT?
- It contains the signature for the JWT.
- It identifies the algorithm used to generate the signature. : this one
- It holds the encrypted data.
- It contains the user’s information.
- How are the parts of a JWT (header, payload, and signature) separated?
- Comma
- Period: this one
- Colon
- Semicolon
- Which algorithm is commonly used for JWT signatures?
- HMAC (Hash-based Message Authentication Code): this one
- RSA (Rivest-Shamir-Adleman)
- AES (Advanced Encryption Standard)
- MD5 (Message Digest Algorithm 5)
- How are JWTs commonly transmitted?
- Only via HTTP headers
- Only as query parameters in the URL
- In the request body as JSON
- Any of the above, depending on the application: this one
- Which part of the JWT contains the actual data (claims)?
import java.util.Base64;
public class DecodeSecret {
public static void main(String[] args) {
String encodedSecret = "bmlnaHRoYXdrY29kaW5nc29jaWV0eWFwY29tcHV0ZXJzY2llbmNlYQ==";
// Decoding the Base64-encoded string
byte[] decodedBytes = Base64.getDecoder().decode(encodedSecret);
String decodedSecret = new String(decodedBytes);
System.out.println("Decoded Secret: " + decodedSecret);
}
}
DecodeSecret.main(null);
Decoded Secret: nighthawkcodingsocietyapcomputersciencea
import java.nio.charset.StandardCharsets;
import java.util.Base64;
public class JwtDecoder {
public static void main(String[] args) {
// Replace with your actual JWT token
String jwtToken = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0b2J5QGdtYWlsLmNvbSIsImlhdCI6MTcwMzA5OTc5MSwiZXhwIjoxNzAzMTE3NzkxfQ.UQw28DUtxf3ketLOFij8PgzP5KCnSdUpvV8W4ZDiMqE";
// Split the JWT into header, payload, and signature
String[] jwtParts = jwtToken.split("\\.");
// Decode the Base64-encoded header and payload
String decodedHeader = decodeBase64Url(jwtParts[0]);
String decodedPayload = decodeBase64Url(jwtParts[1]);
// Print the decoded header and payload
System.out.println("Decoded Header: " + decodedHeader);
System.out.println("Decoded Payload: " + decodedPayload);
}
private static String decodeBase64Url(String base64Url) {
// Add padding if needed before decoding
int padding = 4 - base64Url.length() % 4;
if (padding != 4) {
base64Url = base64Url + "=".repeat(padding);
}
byte[] decodedBytes = Base64.getUrlDecoder().decode(base64Url);
return new String(decodedBytes, StandardCharsets.UTF_8);
}
}
JwtDecoder.main(null);
Decoded Header: {"alg":"HS256"}
Decoded Payload: {"sub":"toby@gmail.com","iat":1703099791,"exp":1703117791}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
public class JwtSignatureExample {
public static void main(String[] args) {
// Secret key for encoding and decoding
String secretKey = "nighthawkcodingsocietyapcomputersciencea";
// Combine Header and Payload with a period '.'
String base64UrlHeader = "base64UrlHeader"; // Placeholder for the actual base64UrlHeader
String base64UrlPayload = "base64UrlPayload"; // Placeholder for the actual base64UrlPayload
String headerPayload = base64UrlHeader + "." + base64UrlPayload;
// Generate HMAC SHA-256 signature
byte[] signature = HmacSha256(headerPayload, secretKey);
// Base64 URL encode the signature
String base64UrlSignature = Base64.getUrlEncoder().encodeToString(signature);
System.out.println("JWT Signature: " + base64UrlSignature);
}
private static byte[] HmacSha256(String data, String key) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKey);
return sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
} catch (NoSuchAlgorithmException | InvalidKeyException e) {
throw new RuntimeException("Error while generating HMAC SHA-256", e);
}
}
}
// Run
JwtSignatureExample.main(null);
JWT Signature: jbCRgHmCs98Ah8n3CWn92WyXU3-RtdDRo9gO5pmm39o=
Call me a genius