Quarkus Security with JWT

How to protect Quarkus Endpoints with a JSON Web Token (JWT)

In that article, I want to show you how to protect Quarkus Endpoints with a JSON Web Token (JWT). The integration of JWT in Quarkus is part of the Framework and extremely easy to implement. To demonstrate this, we are going to provide a Quarkus service with a login endpoint and protect a Rest Resource using roles.

JWT


JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

With token based authentication mechanism, applications are able to authenticate, authorize and verify the identity of a user. Token based authentication works by guarantying that each request to a server is accompanied by a signed token. The server verifies its authenticity and responds to the request if the token is valid. Quarkus integrates the MicroProfile JWT RBAC Security specification to protect services.

Project setup


To setup the project, please have a look at my previous post A first microservice with Quarkus on GraalVM. You’ll find details about the installation of Gradle and GraalVM.

Generate Quarkus Project

Quarkus libraries

To setup a quarkus project, the easiest way is to go to code.quarkus.io. Select the group and the artifact name of your project and select the following dependencies:

  • RESTEasy Jackson
  • Hibernate ORM with Panache
  • JDBC Driver - PostgreSQL
  • YAML Configuration
  • SmallRye JWT
  • SmallRye JWT Build

… and generate your project.

Lombok

I’m using lombok to reduce the boilerplate code of my projects. Please have a look at my previous post A first microservice with Quarkus on GraalVM for more details.

Create the service


Database

For the purpose of this article we are saving user information in the database. To create a database we are going to use a Docker container with a Postgres instance. For more details about installing Postgres in a container, please have a look at my previous article A first microservice with Quarkus on GraalVM.

Create the JPA-Entity

Create a JPA Entity representing a user:

@Entity
@Table(name = "AUTH_USER")
@Getter
@Setter
@ToString(callSuper=true)
@NoArgsConstructor
@AllArgsConstructor
@SequenceGenerator(name=User.USER_SEQ, sequenceName = User.USER_SEQ, allocationSize = 1)
public class User implements Serializable {
    public static final String USER_SEQ = "USER_SEQ";

    public User(@NotNull String email, @NotNull String password, Set<String> roles) {
        this.email = email;
        this.password = password;
        this.roles = roles;
    }

    @Id
    @GeneratedValue(strategy=GenerationType.SEQUENCE, generator=USER_SEQ)
    @Setter(AccessLevel.PRIVATE)
    private Long id;

    @NotNull
    @Column
    protected String password;

    @Email
    @NotNull
    @Column(unique = true, length = 100)
    protected String email;

    @ElementCollection(fetch = FetchType.EAGER)
    private Set<String> roles = new HashSet<>();

}

Bootstrap some data

Create a file called import.sql in src/main/resources with the following content to bootstrap some data into our database:

INSERT INTO public.auth_user (id, email, password) VALUES (1, 'admin@techtalsteve.com', 'F5Pp8OzharO7V8UTwlX8TA==');
INSERT INTO public.user_roles (user_id, roles) VALUES (1, 'ROLE_APPLICATION');

Database configuration and repository

Configure the database connection in src/main/resources/application.yml and create a Panache Repository to fetch a user by its email. Check the source code of the project for more details about the database connection and the Panache repository or have a look at my previous article.

Create SSL certificates

The MicroProfile JWT RBAC specification requires JSON Web Tokens to be signed with the RSA-256 signature algorithm. To achieve this requirement, we will need to generate an RSA public key pair. Quarkus will generate a Token with the RSA private Key and verify its validity with the RSA public key generated from the RSA private key.

Execute the 3 following commands and copy the privateKey.pem and publicKey.pem generated files into src/main/resources/token

$ openssl genrsa -out rsaPrivateKey.pem 2048
$ openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
$ openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem

Create the JWT configuration

As already said previously, Quarkus needs the RSA private key to generate the Token and the RSA public key to verify its validity. This can be achieved by setting the location of the 2 keys in the application.yml with the following properties:

mp:
  jwt:
    verify:
      publickey:
        location: token/publicKey.pem
        issuer: http://techtalksteve.com/issuer
smallrye:
  jwt:
    sign:
      key:
        location: token/privateKey.pem

Note that an issuer has to be set to guarantee the validity of the Token.

Create the services

Crypto Service

For more security, we are going to encrypt the passwords of the user in the database. To achieve this, we are goig to create a java class called CryptoService. We define a key for encryption and decryption and use the Advanced Encryption Standard (AES) symmetric-key encryption algorithm.

@Singleton
public class CryptoService {
    private static final String PADDING = "AES/ECB/PKCS5Padding";
    private final String KEY = "PdSgVkYp3s6v9y/B";

    public String encrypt(final String text) {
        try {
            final SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
            final Cipher cipher = Cipher.getInstance(PADDING);
            cipher.init(Cipher.ENCRYPT_MODE, secretKey);
            byte[] encryptedText = cipher.doFinal(text.getBytes());
            return Base64.getEncoder().encodeToString(encryptedText);
        } catch (Exception e) {
            throw new IllegalStateException("Cannot encrypt text", e);
        }
    }

    public String decrypt(final String encrypted) {
        try {
            final SecretKey secretKey = new SecretKeySpec(KEY.getBytes(), "AES");
            final Cipher cipher = Cipher.getInstance(PADDING);
            cipher.init(Cipher.DECRYPT_MODE, secretKey);
            return new String(cipher.doFinal(Base64.getDecoder().decode(encrypted)));
        } catch (Exception e) {
            throw new IllegalStateException("Cannot decrypt text", e);
        }
    }

}

User Service

The user authentication will be implemented in the UserService.

  1. Verify that the user exists in the database
  2. Verify that the provided password is correct
  3. Generate the JWT-Token with the user roles

Note that the issuer has to match the one we set in the application.yml.

@ApplicationScoped
@Log4j2
@AllArgsConstructor(onConstructor = @__(@Inject))
public class UserService {

    private final UserRepository userRepository;
    private final CryptoService cryptoService;

    public String authenticate(final AuthenticationRequest authRequest)
            throws AuthenticationUsernameException, AuthenticationPasswordException {
        final User user = userRepository.findByEmail(authRequest.getUsername())
                .orElseThrow(AuthenticationUsernameException::new);
        if (user.getPassword().equals(cryptoService.encrypt(authRequest.getPassword()))){
            return generateToken(user);
        }
        throw new AuthenticationPasswordException();
    }

    private String generateToken(final User user) {
        return Jwt.issuer("https://techtalksteve.com/issuer")
                        .upn(user.getEmail())
                        .expiresIn(Duration.ofDays(365))
                        .groups(user.getRoles())
                        .sign();
    }
}

Create the REST-service

Create a class named UserResource to expose the REST API. We will create 2 endpoints to demonstrate how to protect access to the resources.

  1. A login endpoint annotated with @PermitAll to grant the login access to everybody.
  2. A checkRolesAllowed annotated with @RolesAllowed("ROLE_APPLICATION") to restrict the access to that resource for authenticated users having the role ROLE_APPLICATION.
@Path("/api/authentication")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@AllArgsConstructor(onConstructor = @__(@Inject))
public class UserResource {
    private final UserService userService;

    @Inject
    JsonWebToken jwt;

    @PermitAll
    @POST
    @Path("/login")
    @Produces(MediaType.APPLICATION_JSON)
    public Response login(AuthenticationRequest authRequest) throws AuthenticationUsernameException, AuthenticationPasswordException {
        final String token = userService.authenticate(authRequest);
        return Response.ok(new AuthenticationResponse(token)).build();
    }

    @RolesAllowed("ROLE_APPLICATION")
    @GET
    @Path("roles")
    @Produces(MediaType.APPLICATION_JSON)
    public String checkRolesAllowed(){
        return String.format("User %s has the following roles: %s", jwt.getName(), String.join(", ", jwt.getGroups()));
    }

}

Run and test the application

We are now ready to call the application. Execute the following command to start the application:

$ gradle quarkusDev

Test with wrong credentials

The following command calls the login endpoint with the wrong password and returns Wrong password

$ curl -X POST --location "http://localhost:8080/api/authentication/login" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d "{
          \"username\": \"admin@techtalsteve.com\",
          \"password\": \"pass\"
        }"
        
Wrong password

Test with right credentials

The following command calls the login endpoint with the right credentials and returns the Token.

curl -X POST --location "http://localhost:8080/api/authentication/login" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -d "{
          \"username\": \"admin@techtalsteve.com\",
          \"password\": \"password\"
        }"

{"token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RlY2h0YWxrc3RldmUuY29tL2lzc3VlciIsInVwbiI6ImFkbWluQHRlY2h0YWxzdGV2ZS5jb20iLCJncm91cHMiOlsiUk9MRV9BUFBMSUNBVElPTiJdLCJpYXQiOjE2Mjg0Mzg3ODgsImV4cCI6MTY1OTk3NDc4OCwianRpIjoiNmJmOGM3ZDktNDc5Yy00N2FhLWIyMTItYjIyNDhjZmU2NWMxIn0.PUSA4z6eXlBv73j6odS9HZayc2eNvSBgQ2fxb8e6XjL-EWt4aIsoPOIBZqgpe1Kqn5NB2tAFE8ny6NURI1I2uORbQjXJQRGW1Lv-tc7CbOEjel1ehYbLw29NiGYRrNwvyCRpPoutBL4rPnasi4mhTAnTJTTQyMvDP_0PC3s5B_9Gz0bkcrAnCYsDtFNCsVFSYNBMiRZ5wZWNRTqeiEZ1m9ofMyLXdMw-nZxxdLGzu5EhUPN3m9KGZrSxLaBNncugfmSA4ionwbglVcreohlR-_dKN2MDUu9Sak3watz4_jmgrgjZaVr89gGvdRH-z7vrgSMarh6mfPImPp4LhVTWxw"}

Test protected endpoint without token

The following command calls the roles endpoint without a Token and returns 401 Unauthorized.

curl -I --location "http://localhost:8080/api/authentication/roles" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json"

HTTP/1.1 401 Unauthorized

Test protected endpoint with token

The following command calls the roles endpoint with a Token and returns User admin@techtalsteve.com has the following roles: ROLE_APPLICATION.

curl -X GET --location "http://localhost:8080/api/authentication/roles" \
    -H "Accept: application/json" \
    -H "Content-Type: application/json" \
    -H "Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3RlY2h0YWxrc3RldmUuY29tL2lzc3VlciIsInVwbiI6ImFkbWluQHRlY2h0YWxzdGV2ZS5jb20iLCJncm91cHMiOlsiUk9MRV9BUFBMSUNBVElPTiJdLCJpYXQiOjE2Mjg0Mzg3ODgsImV4cCI6MTY1OTk3NDc4OCwianRpIjoiNmJmOGM3ZDktNDc5Yy00N2FhLWIyMTItYjIyNDhjZmU2NWMxIn0.PUSA4z6eXlBv73j6odS9HZayc2eNvSBgQ2fxb8e6XjL-EWt4aIsoPOIBZqgpe1Kqn5NB2tAFE8ny6NURI1I2uORbQjXJQRGW1Lv-tc7CbOEjel1ehYbLw29NiGYRrNwvyCRpPoutBL4rPnasi4mhTAnTJTTQyMvDP_0PC3s5B_9Gz0bkcrAnCYsDtFNCsVFSYNBMiRZ5wZWNRTqeiEZ1m9ofMyLXdMw-nZxxdLGzu5EhUPN3m9KGZrSxLaBNncugfmSA4ionwbglVcreohlR-_dKN2MDUu9Sak3watz4_jmgrgjZaVr89gGvdRH-z7vrgSMarh6mfPImPp4LhVTWxw"

User admin@techtalsteve.com has the following roles: ROLE_APPLICATION

Summary


It’s never easy to secure resources in a Java application but somehow Quarkus succeeded in making things very easy for us. The JWT integration is straight forward and very well implemented and could perfectly fit into your microservice ecosystem.

The source code of the project can be found in github.