Simple JWT Token authentication with Quarkus
Every 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 topic, 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.
I also covered this topic at my Quarkus course in Security section. This articles basically repeats same lesson with additional simplicity
User flow
We’ll take the most usual use case: Users open your app, users register, then they do log in, receive the token, and after that use your app as they please.
Initial setup
You can init empty project from scratch by using next command:
mvn io.quarkus:quarkus-maven-plugin:1.4.2.Final:create \
-DprojectGroupId=tech.donau.quarkify \
-DprojectArtifactId=jwt-auth \
-DclassName="tech.donau.quarkify.ExampleResource" \
-Dpath="/hello" \
-Dextensions="resteasy-jsonb, jwt, jdbc-h2, hibernate-orm-panache"
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>
Complete working solution, as always, available at our github.
Static classes
Let’s start by adding TokenUtils
class, it's independent so you can include it into any project.
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.
And now, let’s create final class, which we’ll use to generate token
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=DonauTech
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 🙂
Let’s also add some more parameters into application.properties
quarkus.datasource.url=jdbc:h2:file:./test quarkus.datasource.driver=org.h2.Driver quarkus.datasource.username=test quarkus.datasource.password=test quarkus.hibernate-orm.database.generation=update quarkus.hibernate-orm.dialect=org.hibernate.dialect.H2Dialect
This is a super simple file database. It’ll be located at target/test.db
. We use update
, which will update schema without deletion of it on restart
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
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.
Let’s go one method at a time:
String hello()
method is just a default method with@PermitAll
annotation. This annotation says that anyone can access this endpoint. Removing it won't change behavior in JWT case.User me()
can only be executed by either User or Service, as@RolesAllowed({Roles.USER, Roles.SERVICE})
says.String adminTest()
can only be executed by Admin. If you try to call it with a User you'll get 403 Unauthenticated response.String nothing()
won't be accessible by anyone.
Test time
Now it’s time to do full test cycle and see how it’ll work. I’ll use curl commands with postman screenshots.
curl --location --request POST 'http://localhost:8080/users/register' \
--header 'Content-Type: application/json' \
--data-raw '{
"login": "testuser",
"email": "testuser@gmail.com",
"password": "testtest"
}'
This will return same result, but you’ll also have your id.
Now let’s log into our application by getting JWT token that we can later use in any other endpoint
curl --location --request GET 'http://localhost:8080/users/login?login=testuser&password=testtest'
Let’s try to execute /hello
without any token, it should work
curl --location --request GET 'http://localhost:8080/hello/'
Now, put our token into header so that key is Authorization
and value is Bearer $token
Now we can execute /hello/me
endpoint, you should see user object in output
curl --location --request GET 'http://localhost:8080/hello/me' \
--header 'Authorization: Bearer $YOURTOKEN'
You can see drawback of storing password as a plain text here. Ideally you shouldn’t return password at all.
Let’s try to execute /hello/admin
endpoint
curl --location --request GET 'http://localhost:8080/hello/admin' \
--header 'Authorization: Bearer $YOURTOKEN'
As expected, simple user won’t be able to access admin endpoints. Try generating admin token yourself and executing this endpoint
And last endpoint, /hello/void
, which won't work for anyone.
curl --location --request GET 'http://localhost:8080/hello/void' \
--header 'Authorization: Bearer $YOURTOKEN'
In conclusion
Now you know embeddable security solution that you can re-use in any project. Of course, there are many other options and this JWT auth doesn’t fit all. But for starters and simple projects, this solution can be really easy, without adding extra services just to handle the basic flow.
Originally published at https://quarkify.net on May 12, 2020.