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 Using OpenID Connect to Protect Service Applications guide if you need to protect your applications using Bearer Token Authorization.
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOME
が適切に設定されていること -
Apache Maven 3.8.1+
-
動作するコンテナランタイム(Docker, Podman)
-
使用したい場合、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
アーキテクチャ
In this example, we will build an application which consists of two JAX-RS 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
or /frontend/admin-name-with-oidc-client
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.
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.git
、 アーカイブ をダウンロードします。
The solution is located in the security-openid-connect-quickstart
directory.
Creating the Maven Project
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
This command generates a Maven project, importing the oidc
, oidc-client-reactive-filter
, oidc-client-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"
これにより、 pom.xml
に以下が追加されます:
<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")
アプリケーションの記述
Let’s start by implementing ProtectedResource
:
package org.acme.security.openid.connect.client;
import javax.annotation.security.RolesAllowed;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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 lets add REST Client with OpenID Connect Client Reactive Filter
and another REST Client with OpenID Connect Token Propagation Filter
, FrontendResource
will use these two clients to call ProtectedResource
:
package org.acme.security.openid.connect.client;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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 ProtectedResourceOidcClientFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
where ProtectedResourceOidcClientFilter
will depend on OidcClientRequestReactiveFilter
to acquire and propagate the tokens and
package org.acme.security.openid.connect.client;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.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 ProtectedResourceTokenPropagationFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
where ProtectedResourceTokenPropagationFilter
will depend on AccessTokenRequestReactiveFilter
to propagate the incoming, already existing tokens.
Note that both ProtectedResourceOidcClientFilter
and ProtectedResourceTokenPropagationFilter
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 javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.smallrye.mutiny.Uni;
@Path("/frontend")
public class FrontendResource {
@Inject
@RestClient
ProtectedResourceOidcClientFilter protectedResourceOidcClientFilter;
@Inject
@RestClient
ProtectedResourceTokenPropagationFilter protectedResourceTokenPropagationFilter;
@GET
@Path("user-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getUserNameWithOidcClientToken() {
return protectedResourceOidcClientFilter.getUserName();
}
@GET
@Path("admin-name-with-oidc-client-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithOidcClientToken() {
return protectedResourceOidcClientFilter.getAdminName();
}
@GET
@Path("user-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getUserNameWithPropagatedToken() {
return protectedResourceTokenPropagationFilter.getUserName();
}
@GET
@Path("admin-name-with-propagated-token")
@Produces("text/plain")
public Uni<String> getAdminNameWithPropagatedToken() {
return protectedResourceTokenPropagationFilter.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
or /frontend/admin-name-with-oidc-client
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, lets add a JAX-RS ExceptionMapper
:
package org.acme.security.openid.connect.client;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.ExceptionMapper;
import javax.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.
アプリケーションの設定
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.ProtectedResourceOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.ProtectedResourceTokenPropagationFilter/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.
|
Keycloak サーバーを起動するにはDockerを使用し、以下のコマンドを実行するだけです。
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
ここで、keycloak.version
は 17.0.0
以上に設定する必要があります。
localhost:8180 で Keycloak サーバーにアクセスできるはずです。
Keycloak 管理コンソールにアクセスするには、 admin
ユーザーとしてログインしてください。ユーザー名は admin
、パスワードは 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.
開発モードでのアプリケーションの実行
アプリケーションを開発モードで実行するには、次を使用します。
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Dev Services for Keycloak は、Keycloak コンテナーを起動し、quarkus-realm.json
をインポートします。
You will be asked to log in into a Single Page Application
provided by OpenID Connect Dev UI
:
-
user
のロールを持つalice
(パスワード:alice
) としてログインします-
accessing
/frontend/user-name-with-propagated-token
will return200
-
accessing
/frontend/admin-name-with-propagated-token
will return403
-
-
ログアウトし、
admin
とuser
ロールの両方を持つadmin
(パスワード:admin
) としててログインします-
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
「dev
モード」で遊び終わったら、標準のJavaアプリケーションとして実行することができます。
まずコンパイルします。
quarkus build
./mvnw clean package
./gradlew build
次に、以下を実行してください。
java -jar target/quarkus-app/quarkus-run.jar
ネイティブモードでのアプリケーションの実行
同じデモをネイティブコードにコンパイルすることができます。
これは、生成されたバイナリーにランタイム技術が含まれており、最小限のリソースオーバーヘッドで実行できるように最適化されているため、本番環境にJVMをインストールする必要がないことを意味します。
コンパイルには少し時間がかかるので、このステップはデフォルトで無効になっています。 native
プロファイルを有効にして再度ビルドしてみましょう。
quarkus build --native
./mvnw package -Dnative
./gradlew build -Dquarkus.package.type=native
コーヒーを飲み終わると、このバイナリーは以下のように直接実行出来るようになります:
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
アプリケーションのテスト
See Running the Application in Dev mode section above about testing your application in dev mode.
curl
を使用して、JVM またはネイティブモードで起動したアプリケーションをテストできます。
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 -v -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 -v -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 -v -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 -v -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 lets 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 -v -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client`
will return 200
status code and the name alice
, but
curl -v -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client`
will return 403
status code.