OpenID Connect Client and Token Propagation Quickstart

このクイックスタートでは、 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)をインストールし、 適切に設定していること

  • jq tool

アーキテクチャ

この例では、 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 clone https://github.com/quarkusio/quarkus-quickstarts.git で Git リポジトリーをクローンします。または、https://github.com/quarkusio/quarkus-quickstarts/archive/2.16.zip[アーカイブ] をダウンロードします。

ソリューションは security-openid-connect-client-quickstart ディレクトリー にあります。

Mavenプロジェクトの作成

まず、新しいプロジェクトが必要です。以下のコマンドで新規プロジェクトを作成します。

コマンドラインインタフェース
quarkus create app org.acme:security-openid-connect-client-quickstart \
    --stream=2.16 \
    --extension='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
    --no-code
cd security-openid-connect-client-quickstart

Gradleプロジェクトを作成するには、 --gradle または --gradle-kotlin-dsl オプションを追加します。

Quarkus CLIのインストール方法や使用方法については、 Quarkus CLIガイド を参照してください。

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:2.16.12.Final:create \
    -DplatformVersion=2.16 \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-openid-connect-client-quickstart \
    -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
    -DnoCode
cd security-openid-connect-client-quickstart

Gradleプロジェクトを作成するには、 -DbuildTool=gradle または -DbuildTool=gradle-kotlin-dsl オプションを追加します。

このコマンドは、 oidc、 oidc-client-reactive-filter、 oidc-token-propagation-reactive-filter 、 resteasy-reactive エクステンションをインポートして Maven プロジェクトを生成します。

すでに Quarkus プロジェクトが設定されている場合は、プロジェクトのベースディレクトリーで以下のコマンドを実行することで、プロジェクトにこれらのエクステンションを追加できます。

CLI
quarkus extension add 'oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
Maven
./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
Gradle
./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>
build.gradle
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 アドレスを指しています。

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.

Keycloak サーバーの起動と設定

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 です。

新しいレルムを作成するには、 レルム設定ファイル をインポートします。 新しいレルムを作成する 方法について詳しくはKeycloakのドキュメントを参照してください。

この quarkus レルムファイルでは、 frontend クライアントと、 alice および admin ユーザーが追加されます。 alice は user ロールを持ち、 admin は user と admin の両方のロールを持ちます。

開発モードでのアプリケーションの実行

アプリケーションを開発モードで実行するには、次を使用します。

CLI
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

Dev Services for Keycloak は、Keycloak コンテナーを起動し、 quarkus-realm.json をインポートします。

/q/dev で入手可能な Dev UI を開き、 OpenID Connect Dev UI の Provider: Keycloak リンクをクリックします。

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アプリケーションとして実行することができます。

まずコンパイルします。

CLI
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

次に、それを実行します:

java -jar target/quarkus-app/quarkus-run.jar

ネイティブモードでのアプリケーションの実行

同じデモをネイティブコードにコンパイルすることができます。

これは、生成されたバイナリーにランタイム技術が含まれており、最小限のリソースオーバーヘッドで実行できるように最適化されているため、本番環境にJVMをインストールする必要がないことを意味します。

コンパイルには少し時間がかかるので、このステップはデフォルトで無効になっています。 native プロファイルを有効にして再度ビルドしてみましょう。

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./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 を返します。