JWT Token authentication with Quarkus
Ahmed Abdelaziz
Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
very significant app will have security. And you probably opened this article because you’re looking for some simple yet durable solution. What can be simpler yet good enough as JWT Token authorization? Official Quarkus website already covered this tobic, but I’ll show the most popular use-case for it, that is, generation of the token when the user logs in. I’ll show you a more abstract solution that you can plug-in to any app and generate a token with a single line of code in your /login method.
Create Quarkus App Or, if you already have some project, you can add next dependencies into pom.xml
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jsonb</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-jdbc-h2</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-hibernate-orm-panache</artifactId> </dependency>
Static class
Let’s start by adding TokenUtils class, it's independent so you can include it into any project
.package com.zizo.security;
import org.eclipse.microprofile.jwt.Claims;
import org.jose4j.jws.AlgorithmIdentifiers;
import org.jose4j.jws.JsonWebSignature;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.NumericDate;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
/**
* Utilities for generating a JWT for testing
*/
public class TokenUtils {
private TokenUtils() {
}
public static String generateTokenString(JwtClaims claims) throws Exception {
// Use the private key associated with the public key for a valid signature
PrivateKey pk = readPrivateKey("/privateKey.pem");
return generateTokenString(pk, "/privateKey.pem", claims);
}
private static String generateTokenString(PrivateKey privateKey, String kid, JwtClaims claims) throws Exception {
long currentTimeInSecs = currentTimeInSecs();
claims.setIssuedAt(NumericDate.fromSeconds(currentTimeInSecs));
claims.setClaim(Claims.auth_time.name(), NumericDate.fromSeconds(currentTimeInSecs));
for (Map.Entry<String, Object> entry : claims.getClaimsMap().entrySet()) {
System.out.printf("\tAdded claim: %s, value: %s\n", entry.getKey(), entry.getValue());
}
JsonWebSignature jws = new JsonWebSignature();
jws.setPayload(claims.toJson());
jws.setKey(privateKey);
jws.setKeyIdHeaderValue(kid);
jws.setHeader("typ", "JWT");
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.RSA_USING_SHA256);
return jws.getCompactSerialization();
}
/**
* Read a PEM encoded private key from the classpath
*
* @param pemResName - key file resource name
* @return PrivateKey
* @throws Exception on decode failure
*/
public static PrivateKey readPrivateKey(final String pemResName) throws Exception {
InputStream contentIS = TokenUtils.class.getResourceAsStream(pemResName);
byte[] tmp = new byte[4096];
int length = contentIS.read(tmp);
return decodePrivateKey(new String(tmp, 0, length, "UTF-8"));
}
/**
* Decode a PEM encoded private key string to an RSA PrivateKey
*
* @param pemEncoded - PEM string for private key
* @return PrivateKey
* @throws Exception on decode failure
*/
public static PrivateKey decodePrivateKey(final String pemEncoded) throws Exception {
byte[] encodedBytes = toEncodedBytes(pemEncoded);
PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedBytes);
KeyFactory kf = KeyFactory.getInstance("RSA");
return kf.generatePrivate(keySpec);
}
private static byte[] toEncodedBytes(final String pemEncoded) {
final String normalizedPem = removeBeginEnd(pemEncoded);
return Base64.getDecoder().decode(normalizedPem);
}
private static String removeBeginEnd(String pem) {
pem = pem.replaceAll("-----BEGIN (.*)-----", "");
pem = pem.replaceAll("-----END (.*)----", "");
pem = pem.replaceAll("\r\n", "");
pem = pem.replaceAll("\n", "");
return pem.trim();
}
/**
* @return the current time in seconds since epoch
*/
public static int currentTimeInSecs() {
long currentTimeMS = System.currentTimeMillis();
return (int) (currentTimeMS / 1000);
}
}
You’re only interested in generateTokenString method. This method will be executed by our future TokenService. You can trace what it's doing, but basically, from an abstraction point of view, this method will encode our JwtClaims object, sign it with a private key that we will generate in next steps(one command line), and return to us it back as a string that we can pass to the user.
Other methods can be handy for some specific use-case, we won’t touch them in this article.
Token service
Now, once we have prepared class that will handle all underlying logic, we can create service that will be used by our services and resources.
JWT embeds idea that every user has role. You can, for example, have user, service and admin roles. Users are basic users of your app, they only have access to user endpoints. We can say that services are bots or applications that have access to limited amount of endpoints. One example can be that service can only upload data, it can’t read or delete it. And admin, of course, is god, he has access to such endpoints that are not available to simple users.
Let’s create a static class with constants for them. This will help you in future if you accidentally will put user instead of User for some endpoint and all the users won't have the ability to execute this endpoint.
package com.zizo.security;
public final class Roles {
private Roles() { }
public static final String USER = "User";
public static final String SERVICE = "Service";
public static final String ADMIN = "Admin";
}
And now, let’s create final class, which we’ll use to generate token
package com.zizo.security;
import org.eclipse.microprofile.jwt.Claims;
import org.jboss.logmanager.Logger;
import org.jose4j.jwt.JwtClaims;
import javax.enterprise.context.RequestScoped;
import java.util.Arrays;
import java.util.UUID;
@RequestScoped
public class TokenService {
public final static Logger LOGGER = Logger.getLogger(TokenService.class.getSimpleName());
public String generateUserToken(String email, String username) {
return generateToken(email, username, Roles.USER);
}
public String generateServiceToken(String serviceId, String serviceName) {
return generateToken(serviceId,serviceName,Roles.SERVICE);
}
public String generateToken(String subject, String name, String... roles) {
try {
JwtClaims jwtClaims = new JwtClaims();
jwtClaims.setIssuer("elzoz"); // change to your company
jwtClaims.setJwtId(UUID.randomUUID().toString());
jwtClaims.setSubject(subject);
jwtClaims.setClaim(Claims.upn.name(), subject);
jwtClaims.setClaim(Claims.preferred_username.name(), name); //add more
jwtClaims.setClaim(Claims.groups.name(), Arrays.asList(roles));
jwtClaims.setAudience("using-jwt");
jwtClaims.setExpirationTimeMinutesInTheFuture(60); // TODO specify how long do you need
String token = TokenUtils.generateTokenString(jwtClaims);
LOGGER.info("TOKEN generated: " + token);
return token;
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
You can see that we have generateUserToken() method that calls generateToken(). The last method executes our TokenUtils.generateTokenString() method, which returns the token.
Private and Public keys
In order to JWT to work, we need to sign it with private key. Without this step anybody can just pass fake data to our Quarkus app, and it will think that it’s valid credentials. This way anybody can use admin endpoints, for example. Luckly get private and public key is super easy:
openssl genrsa -out publicKey.pem openssl pkcs8 -topk8 -inform PEM -in publicKey.pem -out privateKey.pem -nocrypt openssl rsa -in publicKey.pem -pubout -outform PEM -out publicKey.pem
This will create two files, which you can move into src/resources folder. Beware that if you loose this files, users that already generated tokens will be declined in authentication. I would suggest to keep them somewhere safe. Don't share them with anyone as well.
Enable JWT Security
When you moved publicKey.pem and privateKey.pem into src/resources, TokenService will be already usable, but we also need to pass some application properties in order for Quarkus to know where to look for our publicKey.pem. Put next data into application.properties
quarkus.smallrye-jwt.enabled=true mp.jwt.verify.publickey.location=publicKey.pem mp.jwt.verify.issuer=elzoz
Adding persistency
Let’s add User object for our whole article to be usable. I'll use h2 database with Hibernate and Panache to make it as small as possible.
Firstly, let’s create User, as in next example. Keep in mind that it's super simple example, even without password encryption, this is not a production solution
package com.zizo.entities;
import javax.persistence.Entity;
import javax.persistence.Table;
import io.quarkus.hibernate.orm.panache.PanacheEntity;
@Entity
@Table(name = "user")
public class User
extends PanacheEntity {
public String login;
public String email;
public String password;
public String getLogin() {
return login;
}
public void setLogin(String login) {
this.login = login;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}}
Registration and login
We now ready to finally do registration and login. Ideally, we can have UserService, but for this example I just put everything into UserResource
package com.zizo.controllers;
import javax.inject.Inject;
import javax.transaction.Transactional;
import javax.ws.rs.*;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import com.zizo.entities.User;
import com.zizo.security.TokenService;
@Path("/users")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class UserResource {
@Inject
TokenService service;
@POST
@Path("/register")
@Transactional
public User register(User user) {
user.persist(); //super simplified registration, no checks of uniqueness
return user;
}
@GET
@Path("/login")
public String login(@QueryParam("login")String login, @QueryParam("password") String password) {
User existingUser = User.find("login", login).firstResult();
if(existingUser == null || !existingUser.password.equals(password)) {
throw new WebApplicationException(Response.status(404).entity("No user found or password is incorrect").build());
}
return service.generateUserToken(existingUser.email, password);
}
}
There’s nothing special here.
1. We have /register method that just persists the user itself into the database.
2. We have /login method that checks if data is correct, and after that generates a token back to user. Here's a homework for you: make it return a {"token": data} structure so it's easy to use from javascript.
Secure regular endpoints
Security is not a security if it secures nothing. Let’s create few endpoints to test our token. We already have ExampleResource, let's expand it a little bit.
package com.zizo.controllers;
import javax.annotation.security.*;
import javax.ws.rs.*;
import javax.ws.rs.core.*;
import com.zizo.entities.User;
import com.zizo.security.Roles;
@Path("/hello")
@Produces(MediaType.APPLICATION_JSON)
public class ExampleResource {
@Context
SecurityContext securityContext;
@GET
@Produces(MediaType.TEXT_PLAIN)
@PermitAll
public String hello() {
return "hello";
}
@GET
@Path("/me")
@RolesAllowed({Roles.USER, Roles.SERVICE})
public User me() {
return User.find("email", securityContext.getUserPrincipal().getName()).firstResult();
}
@GET
@Path("/admin")
@RolesAllowed(Roles.ADMIN)
public String adminTest() {
return "If you see this text as a user, then something is broke";
}
@GET
@Path("/void")
@DenyAll
public String nothing() {
return "This method should always return 403";
}}
Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
3 年Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
3 年Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
3 年Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
3 年Sr Software Engineer Team Leader at TAMM | Java Champion ??| Java and Web technologies Expert |Solutions Architect | Spring Boot Expert | Microservices | Databases | Expert in and Sprinklr, GENESYS and CISCO CC solutions
3 年