Popcorn hack: list 3 real world applications of JWT:
JSON Web Tokens (JWT) are crucial for secure and efficient user authentication in web development.
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
}
// payload
{
"sub": "123", //example of a registered claim
"name": "jwt lesson",
"iat": 1516239022",
"authorities": [
"ADMIN",
"MANAGER"
],
"extra-claims": "some data here"
}
//signature
{
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
) secret base64 encoded
}
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);
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);
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);
Popcorn hack: write some comments that identify each part of the JWT
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) {
String secretKey = "yourSecretKey"; // secret key - encoding/decoding
String subject = "userId123";
long expirationTimeMillis = System.currentTimeMillis() + 3600000;
String jwt = buildJwt(secretKey, subject, expirationTimeMillis);
System.out.println("Generated JWT: " + jwt);
}
private static String buildJwt(String secretKey, String subject, long expirationTimeMillis) {
// JWT header
String header = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
// JWT payload
String payload = "{\"sub\":\"" + subject + "\",\"iat\":" + System.currentTimeMillis() / 1000 +
",\"exp\":" + expirationTimeMillis / 1000 + "}";
// Base64Url encoding of the JWT header and payload
String encodedHeader = base64UrlEncode(header);
String encodedPayload = base64UrlEncode(payload);
// Concatenate the encoded JWT header and payload using dots
String dataToSign = encodedHeader + "." + encodedPayload;
// creating signature with HmacSHA256
String signature = signData(dataToSign, secretKey);
// Concatenate JWT header and payload with the signature with a dot to form the final JWT
return dataToSign + "." + signature;
}
private static String base64UrlEncode(String input) {
return Base64.getUrlEncoder().encodeToString(input.getBytes(StandardCharsets.UTF_8));
}
private static String signData(String data, String secretKey) {
try {
// Initialize HmacSHA256 with the secret key
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKeySpec);
// Generate the signature by applying HmacSHA256 on the data
byte[] signature = sha256Hmac.doFinal(data.getBytes(StandardCharsets.UTF_8));
// Base64Url encoding of the signature that was generated
return base64UrlEncode(new String(signature, StandardCharsets.UTF_8));
} catch (Exception e) {
throw new RuntimeException("Error signing JWT", e);
}
}
}
JwtGenerator.main(null);
Generated JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VySWQxMjMiLCJpYXQiOjE3MDMwMDE3OTIsImV4cCI6MTcwMzAwNTM5Mn0=.24Tvv71877-9AQjvv71I77-977-977-977-9WO-_ve-_ve-_ve-_vQXvv73vv71bGe-_ve-_ve-_vSHbue-_vQ==
Here is a diagram that makes it easier to understand the process tha happens between JWT and the API calls.
Popcorn Hack: Create your own diagram to help visualize the JWT process
Step 1 (Client - Login Request): The client sends a login request with user credentials (username and password) to the /authenticate endpoint.
Step 2 (JwtApiController):
Step 3 (Client - Subsequent Requests):
Step 4 (JwtRequestFilter):
Step 5 (Spring Security):
Step 6 (Error Handling - JwtAuthenticationEntryPoint):
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).
JWTs are signed to ensure they cannot be modified in transit. Signature is a crucial aspect of JWT security.
Key Usage
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
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
Person database with JWT encryption.
You can see that both the ssn of the user and the password are encrypted, however when requested the SSN does not return. This is intentional. When creating the SSN variable, I put @JsonIgnore
to avoid sending this data to the frontend when requests are made for user data. This is because JWTs aren’t very secure with the use of CSRF attacks, so when a user does login, the server doesn’t always return the SSN of the user. This makes the server more secure to a degree, with SSN only needing to be entered in the very beginning when the user creates their account. When and if the SSN is needed to be used, it doesn’t have to be called to the frontend for comparison. The user simply has to enter the last four digits and since these four digits are sent to the backend with the user name and password, verifications can be made with comparisons made in the backend only.
package com.nighthawk.spring_portfolio.mvc.person;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.vladmihalcea.hibernate.type.json.JsonType;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Convert(attributeName ="person", converter = JsonType.class)
public class Person {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@NotEmpty
@Size(min=5)
@Column(unique=true)
@JsonIgnore
private String ssn;
@NotEmpty
private String password;
@NonNull
@Size(min = 2, max = 30, message = "Name (2 to 30 chars)")
private String name;
public Person(String ssn, String password, String name) {
this.ssn = ssn;
this.password = password;
this.name = name;
}
public static Person[] init() {
Person p1 = new Person();
p1.setName("testName");
p1.setSsn("123-45-6789");
p1.setPassword("testPass123!");
Person persons[] = {p1};
return persons;
}
public static void main(String[] args) {
Person persons[] = init();
for (Person person : persons) {
System.out.println(person);
}
}
}
package com.nighthawk.spring_portfolio.mvc.person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@RestController
@RequestMapping("/api/person")
public class PersonApiController {
@Autowired
private PersonJpaRepository repository;
@Autowired
private PersonDetailsService personDetailsService;
@GetMapping("/")
public ResponseEntity<List<Person>> getPeople() {
return new ResponseEntity<>(repository.findAllByOrderByNameAsc(), HttpStatus.OK);
}
@PostMapping("/post")
public ResponseEntity<Object> postPerson(@RequestParam("ssn") String ssn,
@RequestParam("password") String password,
@RequestParam("name") String name) {
Person person = new Person(ssn, password, name);
personDetailsService.save(person);
return new ResponseEntity<>(name + " is created successfully", HttpStatus.CREATED);
}
}
package com.nighthawk.spring_portfolio.mvc.person;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Service
@Transactional
public class PersonDetailsService implements UserDetailsService {
@Autowired
private PersonJpaRepository personJpaRepository;
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public UserDetails loadUserByUsername(String ssn) throws UsernameNotFoundException {
Person person = personJpaRepository.findBySsn(ssn);
if (person == null) {
throw new UsernameNotFoundException("User not found with SSN: " + ssn);
}
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
return new org.springframework.security.core.userdetails.User(
person.getSsn(),
person.getPassword(),
authorities
);
}
public List<Person> listAll() {
return personJpaRepository.findAllByOrderByNameAsc();
}
public List<Person> list(String name, String ssn) {
return personJpaRepository.findByNameContainingIgnoreCaseOrSsnContainingIgnoreCase(name, ssn);
}
public List<Person> listLike(String term) {
return personJpaRepository.findByNameContainingIgnoreCaseOrSsnContainingIgnoreCase(term, term);
}
public List<Person> listLikeNative(String term) {
String likeTerm = String.format("%%%s%%", term);
return personJpaRepository.findByLikeTermNative(likeTerm);
}
public void save(Person person) {
person.setPassword(passwordEncoder().encode(person.getPassword()));
person.setSsn(passwordEncoder().encode(person.getSsn()));
personJpaRepository.save(person);
}
public Person get(long id) {
return (personJpaRepository.findById(id).isPresent())
? personJpaRepository.findById(id).get()
: null;
}
public Person getBySsn(String ssn) {
return personJpaRepository.findBySsn(ssn);
}
public void delete(long id) {
personJpaRepository.deleteById(id);
}
public void defaults(String password, String ssn, String roleName) {
for (Person person : listAll()) {
if (person.getPassword() == null || person.getPassword().isEmpty() || person.getPassword().isBlank()) {
person.setPassword(passwordEncoder().encode(password));
}
if (person.getSsn() == null || person.getSsn().isEmpty() || person.getSsn().isBlank()) {
person.setSsn(passwordEncoder().encode(ssn));
}
}
}
}
package com.nighthawk.spring_portfolio.mvc.person;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;
public interface PersonJpaRepository extends JpaRepository<Person, Long> {
List<Person> findAllByOrderByNameAsc();
Person findBySsn(String ssn);
List<Person> findByNameContainingIgnoreCaseOrSsnContainingIgnoreCase(String name, String ssn);
@Query(
value = "SELECT * FROM Person p WHERE p.name LIKE ?1 or p.ssn LIKE ?1",
nativeQuery = true)
List<Person> findByLikeTermNative(String term);
}
package com.nighthawk.spring_portfolio.mvc.jwt;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import com.nighthawk.spring_portfolio.mvc.person.Person;
import com.nighthawk.spring_portfolio.mvc.person.PersonDetailsService;
@RestController
@CrossOrigin
public class JwtApiController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private PersonDetailsService personDetailsService;
@PostMapping("/authenticate")
public ResponseEntity<?> createAuthenticationToken(@RequestBody Person authenticationRequest) throws Exception {
String encryptedSsn = authenticationRequest.getSsn();
authenticate(encryptedSsn, authenticationRequest.getPassword());
final UserDetails userDetails = personDetailsService.loadUserByUsername(encryptedSsn);
final String token = jwtTokenUtil.generateToken(userDetails);
final ResponseCookie tokenCookie = ResponseCookie.from("jwt", token)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(3600)
.sameSite("None; Secure")
.build();
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, tokenCookie.toString()).build();
}
private void authenticate(String ssn, String password) throws Exception {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(ssn, password));
} catch (DisabledException e) {
throw new Exception("USER_DISABLED", e);
} catch (BadCredentialsException e) {
throw new Exception("INVALID_CREDENTIALS", e);
} catch (Exception e) {
throw new Exception(e);
}
}
}