The Curious Case of the Tampered Token
A Murder Mystery-Style Guide to JWT Security with Quarkus
It was a quiet evening at API Station when the logs reported something suspicious: unauthorized access to the /secured/admin
endpoint. The guards (aka endpoint interceptors) were baffled. No credentials on record matched the request. And yet, the system accepted the visitor. The signature? Valid.
Detective Quark, the supersonic sleuth from the Subatomic Division, was called in. His first lead? A forged passport disguised as a JWT.
Welcome to the case, developer. You're now Detective Quark's new partner. Together, you'll solve the mystery of the tampered token, uncover how JWTs really work, and set up airtight endpoint security: Quarkus style.
Understanding the Forged Token
Before we catch our culprit, let’s study the weapon: the JWT. It’s short for JSON Web Token—a three-part digital passport, often passed around in HTTP headers like a VIP backstage pass.
A JWT is structured like this:
xxxxx.yyyyy.zzzzz
Header: Algorithm and type (e.g., RS256, JWT).
Payload: Claims about the entity (like
sub
,upn
,groups
, etc.).Signature: The proof. Signed with a private key; verified with a public one.
Forgery happens when this signature is faked or if an API doesn’t verify it properly. That's what we're here to stop.
Assembling the Toolkit
Before we step onto the crime scene, gather your detective gear:
JDK 17+
Maven 3.9.9
A Java IDE (IntelliJ, VSCode, Eclipse)
curl
or Postmanopenssl
Quarkus CLI or Maven command to bootstrap the case.
Create the project:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=jwt-case \
-Dextensions="rest-jackson,smallrye-jwt,smallrye-jwt-build,smallrye-openapi"
cd jwt-case
rest-jackson Gives you REST endpoints and serialization
smallrye-jwt The Jason Web Token features
smallrye-jwt-build Creating JWTs
smallrye-openapi Swagger-UI and much more for convenience.
The Cryptographic Lock
In this mystery, we use asymmetric cryptography. One key locks the door (private key), and another opens it (public key). This ensures that only the token issuer can sign, but anyone can verify.
Quarkus simplifies the setup with smallrye-jwt-build
.
Generate the keys in the src/main/resources directory:
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
An additional step is required to generate and convert the private key to the PKCS#8 format, commonly used for secure key storage and transport.
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
Clues in Configuration
Every detective needs a map. Here’s application.properties
, your investigation guide:
smallrye.jwt.sign.key.location=privateKey.pem
mp.jwt.verify.publickey.location=publicKey.pem
#required property (!) Do not skip this.
mp.jwt.verify.issuer=https://quarkus.io/jwt-case
Meet the Forger
You can’t catch the crook if you don’t know how the forgery works. So we’ll become the forger. Create TokenService.java
to issue JWTs:
@ApplicationScoped
public class TokenService {
public String generateToken(String username, String... roles) {
return Jwt.issuer("https://quarkus.io/jwt-case")
.upn(username)
.groups(new HashSet<>(Arrays.asList(roles)))
.expiresIn(Duration.ofHours(1))
.sign();
}
}
Next, expose this service via /auth/login
:
@Path("/auth")
public class AuthResource {
@Inject TokenService tokenService;
@GET
@Path("/login")
@Produces(MediaType.TEXT_PLAIN)
public Response login(@QueryParam("username") String user, @QueryParam("role") String role) {
if (user == null || role == null) return Response.status(400).entity("Missing params").build();
return Response.ok(tokenService.generateToken(user, role)).build();
}
}
Into the Lion’s Den
The next crime scene: the secured endpoints. Only verified JWTs may enter. Create SecuredResource.java
:
@Path("/secured")
@RequestScoped
public class SecuredResource {
@Inject JsonWebToken jwt;
@Context SecurityContext ctx;
@GET
@Path("/hello")
@RolesAllowed({"user", "admin"})
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
String name = jwt.getName();
boolean isAdmin = jwt.getGroups().contains("admin");
return "Hello " + name + "! Roles: " + jwt.getGroups() + (isAdmin ? " You're an admin." : "");
}
@GET
@Path("/admin")
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "Welcome Admin: " + jwt.getName() + ". Your JWT ID: " + jwt.getTokenID();
}
@GET
@Path("/public")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String publicInfo() {
if (ctx.getUserPrincipal() != null)
return "Hello " + ctx.getUserPrincipal().getName() + "! (token detected)";
return "This is a public endpoint, Guest.";
}
}
The Interrogation
With endpoints ready, let’s run some tests.
Start Quarkus:
quarkus dev
Generate a token for alice as role user:
curl -X 'GET' \
'http://localhost:8080/auth/login?role=user&username=alice' \
-H 'accept: */*'
Inspect on https://jwt.io (and make sure to paste the generated public key to verify).
Generate some more:
curl "http://localhost:8080/auth/login?username=bob&role=admin"
curl "http://localhost:8080/auth/login?username=carol&role=user&role=admin"
Test secured endpoints either with curl or via Swagger-UI.
curl http://localhost:8080/secured/public
curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/secured/hello
curl -H "Authorization: Bearer <TOKEN>" http://localhost:8080/secured/admin
Try invalid tokens. Try missing tokens. See who gets caught.
Case Closed
The mystery is solved. We now understand:
How JWTs are built, signed, and verified.
Why asymmetric keys keep our services safe.
How Quarkus simplifies token creation and verification.
How to lock down endpoints with
@RolesAllowed
.
But beware, detective. The city never sleeps. More crimes await:
What if the keys rotate?
What if an outsider token pretends to be from your issuer?
What if we need token refresh or revocation?
Those are stories for another night.
Epilogue: Where to Go from Here
You’ve just completed your first security case with Quarkus. But don’t stop here.
Try integrating with Keycloak, Vault, or Auth0.
Implement token blacklisting and refresh logic.
Dive deeper into MicroProfile JWT.
Until then, keep your secrets safe and your endpoints safer.