JWT Token authentication with Quarkus

JWT Token authentication with Quarkus

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";

  }}








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

3 年

  • 该图片无替代文字
回复
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

3 年

  • 该图片无替代文字
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

3 年

  • 该图片无替代文字
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

3 年

  • 该图片无替代文字
回复
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

3 年

  • 该图片无替代文字
回复

要查看或添加评论,请登录

Ahmed Abdelaziz的更多文章

  • Java 23

    Java 23

    New and updated Java language features, core API, and the JVM – Java 23 packs it all – for new Java developers to…

  • Kafka Producer And Consumer In Spring Boot

    Kafka Producer And Consumer In Spring Boot

    Appache Kafka is a distributed event streaming platform that is widely used for handling real-time data streams in…

  • Docker with Spring Boot in a simple way

    Docker with Spring Boot in a simple way

    This guide walks you through the process of building a Docker image for running a Spring Boot application. We start…

  • Quarkus Framework and Comparison with Spring Boot

    Quarkus Framework and Comparison with Spring Boot

    In this article, we’ll give an overview of the Quarkus framework and compare it with Spring Boot – the most popular…

  • Spring AI

    Spring AI

    Spring AI supports ChatGPT, the AI language model by OpenAI. ChatGPT has been instrumental in sparking interest in…

  • Aspect Oriented Programming and AOP in Spring Framework

    Aspect Oriented Programming and AOP in Spring Framework

    Aspect-Oriented Programming (AOP) is a programming paradigm that aims to increase modularity by allowing the separation…

    2 条评论
  • Spring Boot Caching with Redis

    Spring Boot Caching with Redis

    Spring Boot Cache Providers Cache providers allow us to transparently and clearly configure the cache in the…

  • The DispatcherServlet

    The DispatcherServlet

    The Engine of Request Handling in Spring Boot. 1.

  • Spring Data with MongoDB

    Spring Data with MongoDB

    1. Overview This article will be a quick and practical introduction to Spring Data MongoDB.

  • Transactionality

    Transactionality

    By default, methods inherited from inherit the transactional configuration from . For read operations, the transaction…

社区洞察

其他会员也浏览了