Quarkus - Using OAuth2 RBAC

This guide explains how your Quarkus application can utilize OAuth2 tokens to provide secured access to the JAX-RS endpoints.

OAuth2 is an authorization framework that enables applications to obtain access to an HTTP resource on behalf of a user.It can be used to implement an application authentication mechanism based on tokens by delegating to an external server (the authentication server) the user authentification and providing a token for the authentication context.

If your OAuth2 Authentication server provides JWT tokens, you should use MicroProfile JWT RBAC instead, this extension aims to be used with opaque tokens and validate the token by calling an introspection endpoint.

Solution

We recommend that you follow the instructions in the next sections and create the application step by step.However, you can go right to the completed example.

Clone the Git repository: git clone https://github.com/quarkusio/quarkus-quickstarts.git, or download an archive.

The solution is located in the security-oauth2-quickstart directory.It contains a very simple UI to use the JAX-RS resources created here, too.

Creating the Maven project

First, we need a new project. Create a new project with the following command:

  1. mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=security-oauth2-quickstart \
  4. -DclassName="org.acme.oauth2.TokenSecuredResource" \
  5. -Dpath="/secured" \
  6. -Dextensions="resteasy-jsonb, security-oauth2"
  7. cd security-oauth2-quickstart

This command generates the Maven project with a REST endpoint and imports the elytron-security-oauth2 extension, which includes the OAuth2 opaque token support.

If you don’t want to use the Maven plugin, you can just include the dependency in your pom.xml:

  1. <dependencies>
  2. <!-- Elytron Security OAuth2 extension -->
  3. <dependency>
  4. <groupId>io.quarkus</groupId>
  5. <artifactId>quarkus-elytron-security-oauth2</artifactId>
  6. </dependency>
  7. </dependencies>

Examine the JAX-RS resource

Open the src/main/java/org/acme/oauth2/TokenSecuredResource.java file and see the following content:

Basic REST Endpoint

  1. package org.acme.oauth2;
  2. import javax.ws.rs.GET;
  3. import javax.ws.rs.Path;
  4. import javax.ws.rs.Produces;
  5. import javax.ws.rs.core.MediaType;
  6. @Path("/secured")
  7. public class TokenSecuredResource {
  8. @GET
  9. @Produces(MediaType.TEXT_PLAIN)
  10. public String hello() {
  11. return "hello";
  12. }
  13. }

This is a basic REST endpoint that does not have any of the Elytron Security OAuth2 specific features, so let’s add some.

We will use the JSR 250 common security annotations, they are described in the Using Security guide.

  1. package org.acme.oauth2;
  2. import java.security.Principal;
  3. import javax.annotation.security.PermitAll;
  4. import javax.inject.Inject;
  5. import javax.ws.rs.GET;
  6. import javax.ws.rs.Path;
  7. import javax.ws.rs.Produces;
  8. import javax.ws.rs.core.Context;
  9. import javax.ws.rs.core.MediaType;
  10. import javax.ws.rs.core.SecurityContext;
  11. @Path("/secured")
  12. @ApplicationScoped
  13. public class TokenSecuredResource {
  14. @GET()
  15. @Path("permit-all")
  16. @PermitAll (1)
  17. @Produces(MediaType.TEXT_PLAIN)
  18. public String hello(@Context SecurityContext ctx) { (2)
  19. Principal caller = ctx.getUserPrincipal(); (3)
  20. String name = caller == null ? "anonymous" : caller.getName();
  21. String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
  22. return helloReply; (4)
  23. }
  24. }
1@PermitAll indicates that the given endpoint is accessible by any caller, authenticated or not.
2Here we inject the JAX-RS SecurityContext to inspect the security state of the call.
3Here we obtain the current request user/caller Principal. For an unsecured call this will be null, so we build the user name by checking caller against null.
4The reply we build up makes use of the caller name, the isSecure() and getAuthenticationScheme() states of the request SecurityContext.

Run the application

Now we are ready to run our application. Use:

  1. ./mvnw compile quarkus:dev

and you should see output similar to:

quarkus:dev Output

  1. $ ./mvnw clean compile quarkus:dev
  2. [INFO] Scanning for projects...
  3. [INFO]
  4. [INFO] ---------------------< org.acme:security-oauth2-quickstart >---------------------
  5. [INFO] Building security-oauth2-quickstart 1.0-SNAPSHOT
  6. [INFO] --------------------------------[ jar ]---------------------------------
  7. ...
  8. [INFO] --- quarkus-maven-plugin:999-SNAPSHOT:dev (default-cli) @ security-oauth2-quickstart ---
  9. Listening for transport dt_socket at address: 5005
  10. 2019-07-16 09:58:09,753 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation
  11. 2019-07-16 09:58:10,884 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 1131ms
  12. 2019-07-16 09:58:11,385 INFO [io.quarkus] (main) Quarkus 0.20.0 started in 1.813s. Listening on: http://[::]:8080
  13. 2019-07-16 09:58:11,391 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, security-oauth2]

Now that the REST endpoint is running, we can access it using a command line tool like curl:

curl command for /secured/permit-all

  1. $ curl http://127.0.0.1:8080/secured/permit-all; echo
  2. hello + anonymous, isSecure: false, authScheme: null

We have not provided any token in our request, so we would not expect that there is any security state seen by the endpoint, and the response is consistent with that:

  • user name is anonymous

  • isSecure is false as https is not used

  • authScheme is null

So now let’s actually secure something. Take a look at the new endpoint method helloRolesAllowed in the following:

  1. package org.acme.oauth2;
  2. import java.security.Principal;
  3. import javax.annotation.security.PermitAll;
  4. import javax.annotation.security.RolesAllowed;
  5. import javax.inject.Inject;
  6. import javax.ws.rs.GET;
  7. import javax.ws.rs.Path;
  8. import javax.ws.rs.Produces;
  9. import javax.ws.rs.core.Context;
  10. import javax.ws.rs.core.MediaType;
  11. import javax.ws.rs.core.SecurityContext;
  12. @Path("/secured")
  13. @ApplicationScoped
  14. public class TokenSecuredResource {
  15. @GET()
  16. @Path("permit-all")
  17. @PermitAll
  18. @Produces(MediaType.TEXT_PLAIN)
  19. public String hello(@Context SecurityContext ctx) {
  20. Principal caller = ctx.getUserPrincipal();
  21. String name = caller == null ? "anonymous" : caller.getName();
  22. String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
  23. return helloReply;
  24. }
  25. @GET()
  26. @Path("roles-allowed") (1)
  27. @RolesAllowed({"Echoer", "Subscriber"}) (2)
  28. @Produces(MediaType.TEXT_PLAIN)
  29. public String helloRolesAllowed(@Context SecurityContext ctx) {
  30. Principal caller = ctx.getUserPrincipal();
  31. String name = caller == null ? "anonymous" : caller.getName();
  32. String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
  33. return helloReply;
  34. }
  35. }
1This new endpoint will be located at /secured/roles-allowed
2@RolesAllowed indicates that the given endpoint is accessible by a caller if they have either a "Echoer" or "Subscriber" role assigned.

After you make this addition to your TokenSecuredResource, try curl -v http://127.0.0.1:8080/secured/roles-allowed; echo to attempt to access the new endpoint. Your output should be:

curl command for /secured/roles-allowed

  1. $ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
  2. * Trying 127.0.0.1...
  3. * TCP_NODELAY set
  4. * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
  5. > GET /secured/roles-allowed HTTP/1.1
  6. > Host: 127.0.0.1:8080
  7. > User-Agent: curl/7.54.0
  8. > Accept: */*
  9. >
  10. < HTTP/1.1 401 Unauthorized
  11. < Connection: keep-alive
  12. < Content-Type: text/html;charset=UTF-8
  13. < Content-Length: 14
  14. < Date: Sun, 03 Mar 2019 16:32:34 GMT
  15. <
  16. * Connection #0 to host 127.0.0.1 left intact
  17. Not authorized

Excellent, we have not provided any OAuth2 token in the request, so we should not be able to access the endpoint, and we were not. Instead we received an HTTP 401 Unauthorized error. We need to obtain and pass in a valid OAuth2 token to access that endpoint. There are two steps to this, 1) configuring our Elytron Security OAuth2 extension with information on how to validate the token, and 2) generating a matching token with the appropriate claims.

Configuring the Elytron Security OAuth2 Extension Security Information

Configuration property fixed at build time - ️ Configuration property overridable at runtime

Configuration propertyTypeDefault
quarkus.oauth2.enabledDetermine if the OAuth2 extension is enabled. Enabled by default if you include the elytron-security-oauth2 dependency, so this would be used to disable it.booleantrue
quarkus.oauth2.client-idThe OAuth2 client id used to validate the token.stringrequired
quarkus.oauth2.client-secretThe OAuth2 client secret used to validate the token.stringrequired
quarkus.oauth2.introspection-urlThe OAuth2 introspection endpoint URL used to validate the token and gather the authentication claims.stringrequired
quarkus.oauth2.ca-cert-fileThe OAuth2 server certificate file. Warning: this is not supported in native mode where the certificate must be included in the truststore used during the native image generation, see Using SSL With Native Executables.string
quarkus.oauth2.role-claimThe claim that is used in the introspection endpoint response to load the roles.stringscope

Setting up application.properties

For part A of step 1, create a security-oauth2-quickstart/src/main/resources/application.properties with the following content:

application.properties for TokenSecuredResource

  1. quarkus.oauth2.client-id=client_id
  2. quarkus.oauth2.client-secret=secret
  3. quarkus.oauth2.introspection-url=http://oauth-server/introspect

You need to specify the introspection URL of your authentication server and the client-id / client-secret that your application will use to authenticate itself to the authentication server.

The extension will then use this information to validate the token and recover the information associate with it.

Generating a token

You need to obtain the token from a standard OAuth2 authentication server (Keycloak for example) using the token endpoint.

You can find below a curl example of such call for a client_credential flow:

  1. curl -X POST "http://oauth-server/token?grant_type=client_credentials" \
  2. -H "Accept: application/json" -H "Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ="

It should respond something like that…​

  1. {"access_token":"60acf56d-9daf-49ba-b3be-7a423d9c7288","token_type":"bearer","expires_in":1799,"scope":"READER"}

Finally, Secured Access to /secured/roles-allowed

Now let’s use this to make a secured request to the /secured/roles-allowed endpoint

curl Command for /secured/roles-allowed With a token

  1. $ curl -H "Authorization: Bearer 60acf56d-9daf-49ba-b3be-7a423d9c7288" http://127.0.0.1:8080/secured/roles-allowed; echo
  2. hello + client_id isSecure: false, authScheme: OAuth2

Success! We now have:

  • a non-anonymous caller name of client_id

  • an authentication scheme of OAuth2

Roles mapping

Roles are mapped from one of the claims of the introspection endpoint response. By default, it’s the scope claim. Roles are obtained by splitting the claim with a space separator. If the claim is an array, no splitting is done, the roles are obtained from the array.

You can customize the name of the claim to use for the roles with the quarkus.oauth2.role-claim property.

Package and run the application

As usual, the application can be packaged using ./mvnw clean package and executed using the -runner.jar file:.Runner jar Example

  1. $ ./mvnw clean package
  2. [INFO] Scanning for projects...
  3. ...
  4. $ java -jar target/security-oauth2-quickstart-runner.jar
  5. 2019-03-28 14:27:48,839 INFO [io.quarkus] (main) Quarkus 0.20.0 started in 0.796s. Listening on: http://[::]:8080
  6. 2019-03-28 14:27:48,841 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, security-oauth2]

You can also generate the native executable with ./mvnw clean package -Pnative..Native Executable Example

  1. $ ./mvnw clean package -Pnative
  2. [INFO] Scanning for projects...
  3. ...
  4. [security-oauth2-quickstart-runner:25602] universe: 493.17 ms
  5. [security-oauth2-quickstart-runner:25602] (parse): 660.41 ms
  6. [security-oauth2-quickstart-runner:25602] (inline): 1,431.10 ms
  7. [security-oauth2-quickstart-runner:25602] (compile): 7,301.78 ms
  8. [security-oauth2-quickstart-runner:25602] compile: 10,542.16 ms
  9. [security-oauth2-quickstart-runner:25602] image: 2,797.62 ms
  10. [security-oauth2-quickstart-runner:25602] write: 988.24 ms
  11. [security-oauth2-quickstart-runner:25602] [total]: 43,778.16 ms
  12. [INFO] ------------------------------------------------------------------------
  13. [INFO] BUILD SUCCESS
  14. [INFO] ------------------------------------------------------------------------
  15. [INFO] Total time: 51.500 s
  16. [INFO] Finished at: 2019-06-28T14:30:56-07:00
  17. [INFO] ------------------------------------------------------------------------
  18. $ ./target/security-oauth2-quickstart-runner
  19. 2019-03-28 14:31:37,315 INFO [io.quarkus] (main) Quarkus 0.20.0 started in 0.006s. Listening on: http://[::]:8080
  20. 2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, security-oauth2]