OpenID Connectクライアントとトークン伝搬クイックスタート
OpenID Connect (OIDC) と OAuth2 クライアントにフィルタを使用して、アプリケーションでアクセストークンを取得、更新、伝播する方法を学びます。
This approach uses an OIDC token propagation Reactive filter to propagate the incoming bearer access tokens.
Quarkus での OIDC Client と Token Propagation のサポートの詳細は、OpenID Connect (OIDC) と OAuth2 クライアントおよびフィルターのリファレンスガイド を参照してください。
Bearer Token Authorization を使用してアプリケーションを保護するには、OpenID Connect (OIDC) ベアラートークン認証 ガイドを参照してください。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 17+がインストールされ、
JAVA_HOMEが適切に設定されていること -
Apache Maven 3.9.6
-
動作するコンテナランタイム(Docker, Podman)
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
アーキテクチャ
In this example, an application is built with two Jakarta REST resources, FrontendResource and ProtectedResource.
Here, FrontendResource uses one of two methods to propagate access tokens to ProtectedResource:
-
It can get a token by using an OIDC token propagation Reactive filter before propagating it.
-
It can use an OIDC token propagation Reactive filter to propagate the incoming access token.
FrontendResource has four 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 uses a REST Client with an OIDC token propagation Reactive filter to get 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.
Also, FrontendResource uses a 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.
ProtectedResource には 2 つのエンドポイントがあります。
-
/protected/user-name -
/protected/admin-name
どちらのエンドポイントも、 FrontendResource から ProtectedResource に伝搬された受信アクセストークンから抽出したユーザー名を返します。これらのエンドポイントの唯一の違いは、 /protected/user-name の呼び出しは、現在のアクセストークンに user ロールがある場合にのみ、 /protected/admin-name の呼び出しは、現在のアクセストークンに admin ロールがある場合のみ許可されることです。
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone -b 3.8 https://github.com/quarkusio/quarkus-quickstarts.git 、 アーカイブ をダウンロードします。
解決策は ` ディレクトリー にあります。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。 以下のコマンドで新規プロジェクトを作成します。
Windowsユーザーの場合:
-
cmdを使用する場合、(バックスラッシュ
\を使用せず、すべてを同じ行に書かないでください)。 -
Powershellを使用する場合は、
-Dパラメータを二重引用符で囲んでください。例:"-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.
すでに Quarkus プロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトにこれらのエクステンションを追加できます。
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 command adds the following extensions 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")
アプリケーションの記述
まず、 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());
}
}
ProtectedResource は userName() と adminName() の両方のメソッドから名前を返します。この名前は、現在の JsonWebToken から抽出されたものです。
Next, add two REST clients, OidcClientRequestReactiveFilter and AccessTokenRequestReactiveFilter, which FrontendResource uses to call ProtectedResource.
Add the OidcClientRequestReactiveFilter REST Client:
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();
}
The RestClientWithOidcClientFilter interface depends on OidcClientRequestReactiveFilter to get and propagate the tokens.
Add the AccessTokenRequestReactiveFilter REST Client:
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();
}
The RestClientWithTokenPropagationFilter interface depends on AccessTokenRequestReactiveFilter to propagate the incoming already-existing tokens.
Note that both RestClientWithOidcClientFilter and RestClientWithTokenPropagationFilter interfaces are the same.
This is because combining OidcClientRequestReactiveFilter and AccessTokenRequestReactiveFilter on the same REST Client causes side effects because both filters can interfere with each other.
For example, OidcClientRequestReactiveFilter can 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 get a new token instead.
次に、 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 uses REST Client with an OIDC token propagation Reactive filter to get 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.
Also, FrontendResource uses 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.
最後に、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 would correctly convert the exceptions that escape from REST Client calls to 500 to avoid leaking the information from the downstream resources such as ProtectedResource.
However, in the tests, it would not be possible to assert that 500 is caused by an authorization exception instead of some internal error.
アプリケーションの設定
コードを準備したら、アプリケーションを設定します。
# 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 ineffective 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 is used by ProtectedResource to verify the incoming access tokens and by OidcClient to get the tokens for a user alice by using a password grant.
Both REST clients point to `ProtectedResource’s HTTP address.
quarkus.oidc.auth-server-url に %prod. プロファイル接頭辞を追加すると、アプリケーションが開発モードまたはテストモードで実行されているときに、 Dev Services for Keycloak がコンテナーを起動するようになります。
詳細は、Running the application in dev mode セクションを参照してください。
|
Keycloak サーバーの起動と設定
アプリケーションを開発モードまたはテストモードで実行するときは、Keycloak サーバーを起動しないでください。 Dev Services for Keycloak がコンテナーを起動します。
詳細は、Running the application in dev mode セクションを参照してください。
必ず、 target/classes ディレクトリーのクラスパス上に レルム設定ファイル を挿入してください。
この配置により、ファイルが開発モードで自動的にインポートされるようになります。
ただし、すでに 全ソリューション を構築している場合は、ビルドプロセスによってすでに実行されているため、レルムファイルをクラスパスに追加する必要はありません。
|
Docker を使用して次のコマンドを実行するだけで、Keycloak サーバーを起動できます。
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
Set {keycloak.version} to 24.0.0 or later.
Keycloak サーバーには localhost:8180 からアクセスできます。
Keycloak 管理コンソールにアクセスするには、 admin ユーザーとしてログインします。
パスワードは admin です。
新しいレルムを作成するには、レルム設定ファイル をインポートします。 新しいレルムを作成する 方法は Keycloakのドキュメントを参照してください。
この quarkus レルムファイルでは、 frontend クライアントと、 alice および admin ユーザーが追加されます。
alice は user ロールを持ち、
admin は user と admin の両方のロールを持ちます。
開発モードでのアプリケーションの実行
アプリケーションを開発モードで実行するには、次を使用します。
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Keycloak の Dev Services は Keycloak コンテナーを起動し、 quarkus-realm.json をインポートします。
Open a Dev UI available at /q/dev-ui and click a Provider: Keycloak link in the OpenID Connect Dev UI card.
求められたら、OpenID Connect Dev UI によって提供される Single Page Application にログインします。
-
Log in as
alice, with the password,alice. This user has auserrole.-
/frontend/user-name-with-propagated-tokenにアクセスすると、200が返されます。 -
/frontend/admin-name-with-propagated-tokenにアクセスすると、403が返されます。
-
-
Log out and back in as
adminwith the password,admin. This user has bothadminanduserroles.-
/frontend/user-name-with-propagated-tokenにアクセスすると、200が返されます。 -
/frontend/admin-name-with-propagated-tokenにアクセスすると、200が返されます。
-
In this case, you are testing that FrontendResource can propagate the access tokens from the OpenID Connect Dev UI.
JVM モードでのアプリケーションの実行
開発モードでアプリケーションを試した後、標準の Java アプリケーションとして実行できます。
まず、コンパイルします:
quarkus build
./mvnw install
./gradlew build
そして、実行してみてください:
java -jar target/quarkus-app/quarkus-run.jar
ネイティブモードでアプリケーションの実行
このデモはネイティブコードにコンパイルできます。変更は必要ありません。
これは、生成されたバイナリーにランタイムテクノロジーが含まれ、 最小限のリソースで実行するように最適化されているため、 実稼働環境に JVM をインストールする必要がなくなることを意味します。
コンパイルには時間がかかるため、この手順はデフォルトでオフになっています。
再度ビルドするには、 native プロファイルを有効にします。
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.package.type=native
しばらくしてビルドが完了すると、ネイティブバイナリーを直接実行できます。
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
アプリケーションのテスト
開発モードでのアプリケーションのテストの詳細は、前述のRunning the application in dev mode セクションを参照してください。
curl を使用して、JVM またはネイティブモードで起動したアプリケーションをテストできます。
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
このコマンドは、ステータスコード 200 と名前 alice を返します。
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
In contrast, this command returns 403.
Recall that alice only has a user role.
次に 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' \
)
Use this token to call /frontend/user-name-with-propagated-token:
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
This command returns a 200 status code and the name admin.
Now, use this token to call /frontend/admin-name-with-propagated-token:
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-propagated-token \
-H "Authorization: Bearer "$access_token
This command also returns the 200 status code and the name admin because admin has both user and admin roles.
Now, check the FrontendResource methods, which do not propagate the existing tokens but use OidcClient to get and propagate the tokens.
As already shown, OidcClient is configured to get the tokens for the alice user, so:
curl -i -X GET \
http://localhost:8080/frontend/user-name-with-oidc-client-token
このコマンドは、ステータスコード 200 と名前 alice を返します。
curl -i -X GET \
http://localhost:8080/frontend/admin-name-with-oidc-client-token
前のコマンドとは対照的に、このコマンドは 403 ステータスコードを返します。