OpenID Connect Client と Token Propagation のクイックスタート
このクイックスタートでは、 OpenID Connect Client Reactive Filter を使用して、アクセストークンを HTTP Authorization Bearer アクセストークンとして取得および伝播する方法と、受信した HTTP Authorization Bearer アクセストークンを伝播する OpenID Token Propagation Reactive Filter を使用する方法を示します。
Quarkus の Oidc Client および Token Propagation のサポートに関する詳細は、OpenID Connect Client と Token Propagation のリファレンスガイド を参照してください。
ベアラートークン認証を使用してアプリケーションを保護する必要がある場合は、OpenID Connect を使用した、サービスアプリケーションの保護 ガイドもお読みください。
前提条件
このガイドを完成させるには、以下が必要です:
-
約15分
-
IDE
-
JDK 11+ がインストールされ、
JAVA_HOMEが適切に設定されていること -
Apache Maven 3.8.6
-
動作するコンテナランタイム(Docker, Podman)
-
使用したい場合は、 Quarkus CLI
-
ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること
アーキテクチャ
この例では、 FrontendResource と ProtectedResource という 2 つの JAX-RS リソースで構成されるアプリケーションを作成します。FrontendResource` は、 OpenID Connect Client Reactive Filter を使用してアクセストークンを取得してからそれを伝播するか、 OpenID Token Propagation Reactive Filter を使用して受信した既存のアクセストークンを伝播して、アクセストークンを ProtectedResource に伝播します。
FrontendResource には 4 つのエンドポイントがあります。
-
/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 は、 /frontend/user-name-with-oidc-client または /frontend/admin-name-with-oidc-client が呼び出された場合、REST Client と OpenID Connect Client Reactive Filter を使用して、 ProtectedResource へのアクセストークンを取得および伝播します。また、 /frontend/user-name-with-propagated-token もしくは /frontend/admin-name-with-propagated-token が呼び出されると、REST Client と OpenID Connect Token Propagation Reactive Filter を使用して、 ProtectedResource への現在の受信アクセストークンを伝播します。
ProtecedResource には 2 つのエンドポイントがあります。
-
/protected/user-name -
/protected/admin-name
これらのエンドポイントはどちらも、 FrontendResource から ProtectedResource に伝搬された受信アクセストークンから抽出したユーザー名を返します。これらのエンドポイントの唯一の違いは、 /protected/user-name の呼び出しは、現在のアクセストークンが user ロールを持つ場合にのみ許可され、 /protected/admin-name の呼び出しは、現在のアクセストークンが admin ロールを持つ場合のみ許可されることです。
ソリューション
次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。
Gitレポジトリをクローンするか git clone https://github.com/quarkusio/quarkus-quickstarts.git 、 アーカイブ をダウンロードします。
ソリューションは security-openid-connect-client-quickstart ディレクトリー にあります。
Mavenプロジェクトの作成
まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。
このコマンドは、 oidc、 oidc-client-reactive-filter、 oidc-token-propagation-reactive-filter 、 resteasy-reactive エクステンションをインポートして Maven プロジェクトを生成します。
すでに 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'
これにより、 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")
アプリケーションの記述
まずは 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());
}
}
ご覧のとおり、 ProtectedResource は userName() と adminName() の両方のメソッドから名前を返します。この名前は、現在の JsonWebToken から抽出されたものです。
次に、 OidcClientRequestReactiveFilter のRESTクライアントと、 AccessTokenRequestReactiveFilter のRESTクライアントを追加しましょう。 FrontendResource は、この2つのクライアントを使用して、 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 RestClientWithOidcClientFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
RestClientWithOidcClientFilter では、 OidcClientRequestReactiveFilter に依存して、トークンを獲得し、伝播させ、
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 RestClientWithTokenPropagationFilter {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName();
@GET
@Produces("text/plain")
@Path("adminName")
Uni<String> getAdminName();
}
RestClientWithTokenPropagationFilter では AccessTokenRequestReactiveFilter に依存して、着信した、すでに存在するトークンを伝播させることになります。
RestClientWithOidcClientFilter と RestClientWithTokenPropagationFilter の両方のインターフェースが同一であることに注意してください。その理由は、同じ REST Client で OidcClientRequestReactiveFilter と AccessTokenRequestReactiveFilter を組み合わせると、両方のフィルターが互いに干渉し合って副作用が発生するからです、 例えば、 OidcClientRequestReactiveFilter は AccessTokenRequestReactiveFilter によって伝播されたトークンを上書きするかもしれませんし、 AccessTokenRequestReactiveFilter は伝播できるトークンがないときに呼ばれると失敗するかもしれませんが、代わりに OidcClientRequestReactiveFilter には新しいトークンを取得することが期待されます。
それでは、 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 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 は、 /frontend/user-name-with-oidc-client または /frontend/admin-name-with-oidc-client が呼び出された場合、REST Client と OpenID Connect Client Reactive Filter を使用して、 ProtectedResource へのアクセストークンを取得および伝播します。また、 /frontend/user-name-with-propagated-token もしくは /frontend/admin-name-with-propagated-token が呼び出されると、REST Client と OpenID Connect Token Propagation Reactive Filter を使用して、 ProtectedResource への現在の受信アクセストークンを伝播します。
最後に、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();
}
}
この例外マッパーは、トークンに期待されるロールがない場合に ProtectedResource が 403 を返すことをテスト中に検証するためにのみ追加されます。このマッパーがないと、 RESTEasy Reactive は、REST クライアント呼び出しからエスケープされる例外を 500 に正しく変換して、 ProtectedResource などのダウンストリームリソースからの情報漏えいを回避しますが、テストでは 500 が実際は内部エラーではなく、認可例外が原因であることをアサートすることはできません。
アプリケーションの設定
コードの準備ができたので、次はアプリケーションを設定します。
# 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
この設定は Keycloak を参照します。この Keycloak は、受信アクセストークンを検証するために ProtectedResource によって使用され、 password グラントを使用してユーザー alice のトークンを取得するために OidcClient によって使用されます。どちらの RESTClient も ProtectedResource の HTTP アドレスを指しています。
quarkus.oidc.auth-server-url に %prod. プロファイル接頭辞を追加すると、アプリケーションが開発モードまたはテストモードで実行されているときに、 Dev Services for Keycloak がコンテナーを起動するようになります。詳細については、以下の 開発モードでのアプリケーションの実行 セクションを参照してください。
|
Keycloak サーバーの起動と設定
アプリケーションを開発モードまたはテストモードで実行しているときは、Keycloak サーバーを起動しないでください。 Dev Services for Keycloak はコンテナーを起動します。詳細については、以下の 開発モードでのアプリケーションの実行 セクションを参照してください。https://github.com/quarkusio/quarkus-quickstarts/tree/2.16/security-openid-connect-client-quickstart/config/quarkus-realm.json[レルム設定ファイル] をクラスパス (target/classes ディレクトリー) に配置して、開発モードで実行しているときに自動的にインポートされるようにします (すでに 完全なソリューション をビルドしている場合を除く (このレルムファイルはビルド時にクラスパスに追加されます))。
|
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 です。
新しいレルムを作成するには、 レルム設定ファイル をインポートします。 新しいレルムを作成する 方法について詳しくはKeycloakのドキュメントを参照してください。
この quarkus レルムファイルでは、 frontend クライアントと、 alice および admin ユーザーが追加されます。 alice は user ロールを持ち、 admin は user と admin の両方のロールを持ちます。
開発モードでのアプリケーションの実行
アプリケーションを開発モードで実行するには、次を使用します。
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
Dev Services for Keycloak は、Keycloak コンテナーを起動し、 quarkus-realm.json をインポートします。
OpenID Connect Dev UI が提供する Single Page Application へのログインを求められます。
-
userのロールを持つalice(パスワード:alice) としてログインします-
/frontend/user-name-with-propagated-tokenにアクセスすると200が返されます -
/frontend/admin-name-with-propagated-tokenにアクセスすると403が返されます
-
-
ログアウトし、
adminとuserロールの両方を持つadmin(パスワード:admin) としててログインします-
/frontend/user-name-with-propagated-tokenにアクセスすると200が返されます -
/frontend/admin-name-with-propagated-tokenにアクセスすると200が返されます
-
この場合、 FrontendResource が OpenID Connect Dev UI により取得されたアクセストークンを伝搬できるかどうかをテストしています。
JVM モードでのアプリケーションの実行
「dev モード」で遊び終わったら、標準の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
アプリケーションのテスト
開発モードでのアプリケーションのテストについては、上記の 開発モードでのアプリケーションの実行 セクションを参照してください。
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' \
)
このトークンを使用して、 /frontend/user-name-with-propagated-token と /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
403 を返します - alice には user ロールしかないことを思い出してください。
次に 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' \
)
このトークンを使用して、 /frontend/user-name-with-propagated-token と /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
admin には user ロールと admin ロールの両方があるため、 200 ステータスコードと名前 admin を返します。
ここで、既存のトークンは伝播しないが、 OidcClient を使用してトークンを取得および伝播する FrontendResource メソッドを見てみましょう。 OidcClient は alice ユーザーのトークンを取得するように設定されています。
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 を返します。