java
49 lines · 9 steps
How HMAC-signed JWTs are created and verified
A compact Java class that signs JWTs with HMAC-SHA256 and verifies them safely against tampering and expiry.
Explained by
highlit
1public final class HmacJwt {
2
3 private static final ObjectMapper MAPPER = new ObjectMapper();
4 private static final Base64.Encoder ENCODER = Base64.getUrlEncoder().withoutPadding();
5 private static final Base64.Decoder DECODER = Base64.getUrlDecoder();
6
7 private final SecretKeySpec key;
8
9 public HmacJwt(String secret) {
10 this.key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
11 }
12
13 public String sign(Map<String, Object> claims) throws Exception {
14 Map<String, Object> header = Map.of("alg", "HS256", "typ", "JWT");
15 String signingInput = encode(header) + "." + encode(claims);
16 String signature = ENCODER.encodeToString(hmac(signingInput));
17 return signingInput + "." + signature;
18 }
19
20 public Map<String, Object> verify(String token) throws Exception {
21 String[] parts = token.split("\\.");
22 if (parts.length != 3) {
23 throw new SecurityException("malformed token");
24 }
25 String signingInput = parts[0] + "." + parts[1];
26 byte[] expected = hmac(signingInput);
27 byte[] provided = DECODER.decode(parts[2]);
28 if (!MessageDigest.isEqual(expected, provided)) {
29 throw new SecurityException("signature mismatch");
30 }
31 Map<String, Object> claims = MAPPER.readValue(DECODER.decode(parts[1]),
32 new TypeReference<Map<String, Object>>() {});
33 Object exp = claims.get("exp");
34 if (exp instanceof Number n && Instant.now().getEpochSecond() > n.longValue()) {
35 throw new SecurityException("token expired");
36 }
37 return claims;
38 }
39
40 private byte[] hmac(String data) throws Exception {
41 Mac mac = Mac.getInstance("HmacSHA256");
42 mac.init(key);
43 return mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
44 }
45
46 private String encode(Object value) throws Exception {
47 return ENCODER.encodeToString(MAPPER.writeValueAsBytes(value));
48 }
49}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A JWT is just three base64url segments joined by dots, with the last being an HMAC over the first two.
- 2Signature comparison must use a constant-time check like MessageDigest.isEqual to avoid timing attacks.
- 3Verification is only trustworthy when it re-derives the signature from the received bytes and also enforces expiry.
Related explainers
java
public interface GitHubClient { @GetExchange("/users/{username}") GitHubUser getUser(@PathVariable String username);
Declarative HTTP clients in Spring
http-client
declarative-api
proxy
Intermediate
8 steps
java
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map;
Building a trie for autocomplete in Java
trie
prefix-tree
recursion
Intermediate
8 steps
java
@RestController @RequestMapping("/api/products") public class ProductSearchController {
Binding collection query params in Spring
rest-api
query-parameters
dependency-injection
Intermediate
6 steps
java
import java.util.ArrayDeque; import java.util.Deque; import java.util.Map;
Evaluating math expressions with two stacks
stacks
parsing
operator-precedence
Intermediate
9 steps
java
@Entity @Table(name = "orders") @SQLDelete(sql = "UPDATE orders SET deleted = true, deleted_at = now() WHERE id = ?") @Where(clause = "deleted = false")
Soft deletes with Hibernate in Spring
soft-delete
jpa
hibernate
Intermediate
9 steps
ruby
require "base64" require "json" module Attachment
Packing binary attachments into JSON
serialization
base64
json
Intermediate
8 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/how-hmac-signed-jwts-are-created-and-verified-explained-java-604b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.