OpenID Connect Client and Token Propagation Quickstart
This quickstart demonstrates how to use OpenID Connect Client Reactive
Filter
to acquire and propagate access tokens as HTTP Authorization
Bearer
access tokens, alongside OpenID Token Propagation Reactive Filter
which propagates the incoming HTTP Authorization Bearer
access tokens.
Please check OpenID
Connect Client and Token Propagation Reference Guide for all the
information related to Oidc Client
and Token Propagation
support in
Quarkus.
Please also read OIDC Bearer token authentication guide if you need to protect your applications using Bearer Token Authorization.
准备
要完成本指南,您需要:
-
Roughly 15 minutes
-
An IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.9.6
-
A working container runtime (Docker or Podman)
-
Optionally the Quarkus CLI if you want to use it
-
Optionally Mandrel or GraalVM installed and configured appropriately if you want to build a native executable (or Docker if you use a native container build)
架构
In this example, we will build an application which consists of two Jakarta
REST resources, FrontendResource
and
ProtectedResource
. FrontendResource
propagates access tokens to
ProtectedResource
and uses either OpenID Connect Client Reactive Filter
to acquire a token first before propagating it or OpenID Token Propagation
Reactive Filter
to propagate the incoming, already existing access token.
FrontendResource
has 4 endpoints:
-
/frontend/user-name-with-oidc-client-token
-
/frontend/admin-name-with-oidc-client-token
-
/frontend/user-name-with-propagated-token
-
/frontend/admin-name-with-propagated-token
FrontendResource
will use REST Client with OpenID Connect Client Reactive
Filter
to acquire and propagate an access token to ProtectedResource
when
either /frontend/user-name-with-oidc-client-token
or
/frontend/admin-name-with-oidc-client-token
is called. And it will use
REST Client with OpenID Connect Token Propagation Reactive Filter
to
propagate the current incoming access token to ProtectedResource
when
either /frontend/user-name-with-propagated-token
or
/frontend/admin-name-with-propagated-token
is called.
ProtecedResource
has 2 endpoints:
-
/protected/user-name
-
/protected/admin-name
Both of these endpoints return the username extracted from the incoming
access token which was propagated to ProtectedResource
from
FrontendResource
. The only difference between these endpoints is that
calling /protected/user-name
is only allowed if the current access token
has a user
role and calling /protected/admin-name
is only allowed if the
current access token has an admin
role.
完整源码
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-openid-connect-client-quickstart
directory.
Creating the Maven Project
First, we need a new project. Create a new project with the following command:
For Windows users:
-
If using cmd, (don’t use backward slash
\
and put everything on the same line) -
If using Powershell, wrap
-D
parameters in double quotes e.g."-DprojectArtifactId=security-openid-connect-client-quickstart"
This command generates a Maven project, importing the oidc
,
oidc-client-reactive-filter
, oidc-token-propagation-reactive-filter
and
resteasy-reactive
extensions.
If you already have your Quarkus project configured, you can add these extensions to your project by running the following command in your project base directory:
quarkus extension add oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive
./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
./gradlew addExtension --extensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
This will add the following to your build file:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-reactive</artifactId>
</dependency>
implementation("io.quarkus:quarkus-oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive")
Writing the application
Let’s start by implementing ProtectedResource
:
package org.acme.security.openid.connect.client;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/protected")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken principal;
@GET
@RolesAllowed("user")
@Produces("text/plain")
@Path("userName")
public Uni<String> userName() {
return Uni.createFrom().item(principal.getName());
}
@GET
@RolesAllowed("admin")
@Produces("text/plain")
@Path("adminName")
public Uni<String> adminName() {
return Uni.createFrom().item(principal.getName());
}
}
As you can see ProtectedResource
returns a name from both userName()
and
adminName()
methods. The name is extracted from the current
JsonWebToken
.
Next let’s add a REST Client with OidcClientRequestReactiveFilter
and
another REST Client with
AccessTokenRequestReactiveFilter
. FrontendResource
will use these two
clients to call ProtectedResource
:
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface RestClientWithOidcClientFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
where RestClientWithOidcClientFilter
will depend on
OidcClientRequestReactiveFilter
to acquire and propagate the tokens and
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface RestClientWithTokenPropagationFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
where RestClientWithTokenPropagationFilter
will depend on
AccessTokenRequestReactiveFilter
to propagate the incoming, already
existing tokens.
Note that both RestClientWithOidcClientFilter
and
RestClientWithTokenPropagationFilter
interfaces are identical - the reason
behind it is that combining OidcClientRequestReactiveFilter
and
AccessTokenRequestReactiveFilter
on the same REST Client will cause side
effects as both filters can interfere with other, for example,
OidcClientRequestReactiveFilter
may override the token propagated by
AccessTokenRequestReactiveFilter
or AccessTokenRequestReactiveFilter
can
fail if it is called when no token is available to propagate and
OidcClientRequestReactiveFilter
is expected to acquire a new token
instead.
Now let’s complete creating the application with adding FrontendResource
:
package org.acme.security.openid.connect.client;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.smallrye.mutiny.Uni;
@Path("/frontend")
public class FrontendResource {
@Inject
@RestClient
RestClientWithOidcClientFilter restClientWithOidcClientFilter;
@Inject
@RestClient
RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter;
@GET
@Path("user-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientToken() {
return restClientWithOidcClientFilter.getUserName();
}
@GET
@Path("admin-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientToken() {
return restClientWithOidcClientFilter.getAdminName();
}
@GET
@Path("user-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getUserNameWithPropagatedToken() {
return restClientWithTokenPropagationFilter.getUserName();
}
@GET
@Path("admin-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithPropagatedToken() {
return restClientWithTokenPropagationFilter.getAdminName();
}
}
FrontendResource
will use REST Client with OpenID Connect Client Reactive
Filter
to acquire and propagate an access token to ProtectedResource
when
either /frontend/user-name-with-oidc-client-token
or
/frontend/admin-name-with-oidc-client-token
is called. And it will use
REST Client with OpenID Connect Token Propagation Reactive Filter
to
propagate the current incoming access token to ProtectedResource
when
either /frontend/user-name-with-propagated-token
or
/frontend/admin-name-with-propagated-token
is called.
Finally, let’s add a Jakarta REST ExceptionMapper
:
package org.acme.security.openid.connect.client;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import org.jboss.resteasy.reactive.ClientWebApplicationException;
@Provider
public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplicationException> {
@Override
public Response toResponse(ClientWebApplicationException t) {
return Response.status(t.getResponse().getStatus()).build();
}
}
This exception mapper is only added to verify during the tests that
ProtectedResource
returns 403
when the token has no expected
role. Without this mapper RESTEasy Reactive
will correctly convert the
exceptions which will escape from REST Client calls to 500
to avoid
leaking the information from the downstream resources such as
ProtectedResource
but in the tests it will not be possible to assert that
500
is in fact caused by an authorization exception as opposed to some
internal error.
Configuring the application
We have prepared the code, and now let’s configure the application:
# Configure OIDC
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret
# Tell Dev Services for Keycloak to import the realm file
# This property is not effective when running the application in JVM or Native modes but only in dev and test modes.
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
# Configure OIDC Client
quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice
# Configure REST Clients
%prod.port=8080
%dev.port=8080
%test.port=8081
org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
This configuration references Keycloak which will be used by
ProtectedResource
to verify the incoming access tokens and by OidcClient
to get the tokens for a user alice
using a password
grant. Both
RESTClients point to `ProtectedResource’s HTTP address.
Adding a %prod. profile prefix to quarkus.oidc.auth-server-url ensures
that Dev Services for Keycloak will launch a container for you when the
application is run in dev or test modes. See Running
the Application in Dev mode section below for more information.
|
Starting and Configuring the Keycloak Server
Do not start the Keycloak server when you run the application in dev mode or
test modes - Dev Services for Keycloak will launch a container. See
Running the Application in Dev mode section below
for more information. Make sure to put the
realm
configuration file on the classpath (target/classes directory) so that it
gets imported automatically when running in dev mode - unless you have
already built a
complete
solution in which case this realm file will be added to the classpath
during the build.
|
To start a Keycloak Server you can use Docker and just run the following command:
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
where keycloak.version
should be set to 17.0.0
or higher.
You should be able to access your Keycloak Server at localhost:8180.
Log in as the admin
user to access the Keycloak Administration
Console. Username should be admin
and password admin
.
Import the realm configuration file to create a new realm. For more details, see the Keycloak documentation about how to create a new realm.
This quarkus
realm file will add a frontend
client, and alice
and
admin
users. alice
has a user
role, admin
- both user
and admin
roles.
Running the Application in Dev mode
To run the application in a dev mode, use:
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Dev Services for Keycloak
will launch a Keycloak container and import a quarkus-realm.json
.
Open a Dev UI available at
/q/dev-ui and click on a Provider:
Keycloak
link in an OpenID Connect
Dev UI
card.
You will be asked to log in into a Single Page Application
provided by
OpenID Connect Dev UI
:
-
Login as
alice
(password:alice
) who has auser
role-
accessing
/frontend/user-name-with-propagated-token
will return200
-
accessing
/frontend/admin-name-with-propagated-token
will return403
-
-
Logout and login as
admin
(password:admin
) who has bothadmin
anduser
roles-
accessing
/frontend/user-name-with-propagated-token
will return200
-
accessing
/frontend/admin-name-with-propagated-token
will return200
-
In this case you are testing that FrontendResource
can propagate the
access tokens acquired by OpenID Connect Dev UI
.
Running the Application in JVM mode
When you’re done playing with the dev
mode" you can run it as a standard
Java application.
First compile it:
quarkus build
./mvnw install
./gradlew build
Then run it:
java -jar target/quarkus-app/quarkus-run.jar
Running the Application in Native Mode
This same demo can be compiled into native code: no modifications required.
This implies that you no longer need to install a JVM on your production environment, as the runtime technology is included in the produced binary, and optimized to run with minimal resource overhead.
Compilation will take a bit longer, so this step is disabled by default;
let’s build again by enabling the native
profile:
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
After getting a cup of coffee, you’ll be able to run this binary directly:
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
Testing the Application
See Running the Application in Dev mode section above about testing your application in dev mode.
You can test the application launched in JVM or Native modes with curl
.
Obtain an access token for alice
:
export access_token=$(\
curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
)
Now use this token to call /frontend/user-name-with-propagated-token
and
/frontend/admin-name-with-propagated-token
:
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
will return 200
status code and the name alice
while
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
will return 403
- recall that alice
only has a user
role.
Next obtain an access token for admin
:
export access_token=$(\
curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
--user backend-service:secret \
-H 'content-type: application/x-www-form-urlencoded' \
-d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
)
and use this token to call /frontend/user-name-with-propagated-token
and
/frontend/admin-name-with-propagated-token
:
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
will return 200
status code and the name admin
, and
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
will also return 200
status code and the name admin
, as admin
has both
user
and admin
roles.
Now let’s check FrontendResource
methods which do not propagate the
existing tokens but use OidcClient
to acquire and propagate the
tokens. You have seen that OidcClient
is configured to acquire the tokens
for the alice
user, so:
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client-token
will return 200
status code and the name alice
, but
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token
will return 403
status code.