Security with JWT RBAC

Securing endpoints with JWT RBAC

In a microservices architecture, and generally speaking, any application, might need to be protected so only specific users can access the defined endpoint. Quarkus provides integration to the MicroProfile JWT RBAC spec.

So let’s see how you can start using JWT for Role Based Access Control (RBAC) of endpoints.

Add the JWT extension

Just open a new terminal window, and make sure you’re at the root of your tutorial-app project, then run:

  • Maven

  • Quarkus CLI

./mvnw quarkus:add-extension -D"extension=quarkus-smallrye-jwt"
quarkus extension add quarkus-smallrye-jwt

Add the JWT properties

Add the following properties to your application.properties in src/main/resources:

mp.jwt.verify.publickey.location=https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.pub
mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac

#set jwt expiration duration
#com.developers.redhat.jwt.duration=3600

We are providing a valid token that can be verified by the configured public key:

{
  "kid": "/privateKey.pem",
  "typ": "JWT",
  "alg": "RS256"
},
{
  "sub": "jdoe-using-jwt-rbac",
  "aud": "using-jwt-rbac",
  "upn": "jdoe@quarkus.io",
  "birthdate": "2001-07-13",
  "auth_time": 1570094171,
  "iss": "https://quarkus.io/using-jwt-rbac", (1)
  "roleMappings": {
    "group2": "Group2MappedRole",
    "group1": "Group1MappedRole"
  },
  "groups": [ (2)
    "Echoer",
    "Tester",
    "Subscriber",
    "group2"
  ],
  "preferred_username": "jdoe",
  "exp": 2200814171,
  "iat": 1570094171,
  "jti": "a-123"
}
1 The issuer you set in application.properties
2 groups field is used by MicroProfile JWT RBAC to get the access groups (or roles) that the owner of the token has

Create SecureResource

You can inject any defined claim into an object by using @Claim annotation:

Change the SecureResource Java class in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

@Path("secure")
public class SecureResource {

    @Claim(standard = Claims.preferred_username)
    String username;

    @GET
    @Path("claim")
    public String getClaim() {
        return username;
    }

}

Invoke the /secure/claim endpoint

Run the following command in a bash shell:

token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s)
curl -H "Authorization: Bearer $token" localhost:8080/secure/claim

You should see the preferred_username field for the given token (jdoe).

jdoe

MicroProfile JWT RBAC spec is providing out-of-the-box validation of the given token. These validations include, for example, that the token has not been modified, has not expired, or the issuer is the expected one.

To validate this, just invoke the service again, but change the token:

token=XXXX
curl -v -H "Authorization: Bearer $token" localhost:8080/secure/claim
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /secure/claim HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: Bearer XXXX
>
< HTTP/1.1 401 Unauthorized
< www-authenticate: Bearer {token}
< content-length: 0
<
* Connection #0 to host localhost left intact
* Closing connection 0

You can check the 401 Unauthorized response.

Add RBAC to SecureResource

So far, you’ve seen how to get claims from the provided JWT token, but anyone could access that endpoint, so let’s protect it with a role. For this case you need to use a role that is defined in the JWT token inside the groups claim (ie Subscriber).

Change the SecureResource Java class in src/main/java in the com.redhat.developers package with the following contents:

package com.redhat.developers;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

@Path("/secure")
public class SecureResource {

    @Claim(standard = Claims.preferred_username)
    String username;

    @RolesAllowed("Subscriber")
    @GET
    @Path("/claim")
    public String getClaim() {
        return username;
    }

}

Invoke the /secure/claim endpoint with RBAC

Run the following command:

token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s)
curl -H "Authorization: Bearer $token" localhost:8080/secure/claim

And you’ll see the preferred_username field for the given token (jdoe).

jdoe

Add incorrect RBAC to SecureResource

package com.redhat.developers;

import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

@Path("/secure")
public class SecureResource {

    @Claim(standard = Claims.preferred_username)
    String username;

    @RolesAllowed("Not-Subscriber")
    @GET
    @Path("/claim")
    public String getClaim() {
        return username;
    }

}

Invoke the /secure/claim endpoint with incorrect RBAC

Run the following command:

token=$(curl https://raw.githubusercontent.com/redhat-developer-demos/quarkus-tutorial/master/jwt-token/quarkus.jwt.token -s)
curl -v -H "Authorization: Bearer $token" localhost:8080/secure/claim

And you’ll see the preferred_username field for the given token (jdoe).

*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /secure/claim HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
> Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTcwMDk0MTcxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MjIwMDgxNDE3MSwiaWF0IjoxNTcwMDk0MTcxLCJqdGkiOiJhLTEyMyJ9.Hzr41h3_uewy-g2B-sonOiBObtcpkgzqmF4bT3cO58v45AIOiegl7HIx7QgEZHRO4PdUtR34x9W23VJY7NJ545ucpCuKnEV1uRlspJyQevfI-mSRg1bHlMmdDt661-V3KmQES8WX2B2uqirykO5fCeCp3womboilzCq4VtxbmM2qgf6ag8rUNnTCLuCgEoulGwTn0F5lCrom-7dJOTryW1KI0qUWHMMwl4TX5cLmqJLgBzJapzc5_yEfgQZ9qXzvsT8zeOWSKKPLm7LFVt2YihkXa80lWcjewwt61rfQkpmqSzAHL0QIs7CsM9GfnoYc0j9po83-P3GJiBMMFmn-vg
>
< HTTP/1.1 403 Forbidden
< Content-Length: 9
< Content-Type: application/octet-stream
<
* Connection #0 to host localhost left intact
Forbidden* Closing connection 0

You can notice the 403 Forbidden response.