The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.

OAuth2 RBACの使用

このガイドでは、QuarkusアプリケーションがOAuth2トークンを利用して、Jakarta REST(旧称JAX-RS)エンドポイントへのセキュアなアクセスを提供する方法について説明します。

OAuth2は、アプリケーションがユーザーに代わってHTTPリソースへのアクセスを取得することを可能にする認可フレームワークです。ユーザー認証を外部サーバー(認証サーバー)に委譲し、認証コンテキストにトークンを提供することで、トークンに基づくアプリケーション認証の仕組みを実装することができます。

このエクステンションは、opaque形式のBearerトークンを使用してイントロスペクションエンドポイントを呼び出して検証するための軽量サポートを提供します。

OAuth2認証サーバーがJWTベアラートークンを提供する場合は、代わりに OIDCベアラートークン認証 または SmallRye JWT エクステンションの使用を検討してください。QuarkusアプリケーションでOIDC認可コードフローを使用してユーザーを認証する必要がある場合は、OpenID Connectエクステンションを使用する必要があります。詳細については、 OIDCコードフローメカニズムによるWebアプリケーションの保護 ガイドを参照してください。

ソリューション

次の章で紹介する手順に沿って、ステップを踏んでアプリを作成することをお勧めします。ただし、完成した例にそのまま進んでも構いません。

Git リポジトリのクローン: git clone https://github.com/quarkusio/quarkus-quickstarts.git 、またはアーカイブをダウンロードしてください。

ソリューションは security-oauth2-quickstart ディレクトリ にあります。ここで作成された Jakarta REST リソースを使用するための非常にシンプルな UI も含まれています。

Mavenプロジェクトの作成

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

コマンドラインインタフェース
quarkus create app org.acme:security-oauth2-quickstart \
    --extension='rest-jackson,security-oauth2' \
    --no-code
cd security-oauth2-quickstart

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

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

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.10.1:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-oauth2-quickstart \
    -Dextensions='rest-jackson,security-oauth2' \
    -DnoCode
cd security-oauth2-quickstart

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

Windowsユーザーの場合:

  • cmdを使用する場合、(バックスラッシュ \ を使用せず、すべてを同じ行に書かないでください)。

  • Powershellを使用する場合は、 -D パラメータを二重引用符で囲んでください。例: "-DprojectArtifactId=security-oauth2-quickstart"

このコマンドはプロジェクトを生成し、OAuth2のopaqueトークンのサポートを含む elytron-security-oauth2 エクステンションをインポートします。

Mavenプラグインを使用したくない場合は、ビルドファイルに依存関係を含めるだけでよいです:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-elytron-security-oauth2</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-elytron-security-oauth2")

Jakarta REST リソースの調査

以下の内容で src/main/java/org/acme/security/oauth2/TokenSecuredResource.java ファイルを作成します。

package org.acme.security.oauth2;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;

@Path("/secured")
public class TokenSecuredResource {

    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String hello() {
        return "hello";
    }
}

これは Elytron Security OAuth2 特有の機能を持たない基本的なRESTエンドポイントなので、いくつか追加してみましょう。

JSR250の一般的なセキュリティアノテーションを使用します。これらのアノテーションは、 セキュリティの使用 ガイドで説明されています。

package org.acme.security.oauth2;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {


    @GET()
    @Path("permit-all")
    @PermitAll (1)
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) { (2)
        Principal caller =  ctx.getUserPrincipal(); (3)
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply; (4)
    }
}
1 @PermitAll は、認証されているかどうかに関わらず、どのような呼出元からでもアクセス可能であることを示しています。
2 ここでは、呼び出しのセキュリティ状態を検査するために、jakarta REST SecurityContext を注入しています。
3 ここでは、現在のリクエストの ユーザー/呼出元である Principal を取得します。セキュリティー保護されていない呼出の場合、これはnullになりますので、 caller をnullかチェックしてユーザー名を作成します。
4 私たちが作成した応答では 呼出元の名前、リクエストの SecurityContextisSecure()getAuthenticationScheme() の状態を利用しています。

application.propertiesの設定

アプリケーションには、以下の最小限のプロパティーを設定する必要があります:

quarkus.oauth2.client-id=client_id
quarkus.oauth2.client-secret=secret
quarkus.oauth2.introspection-url=http://oauth-server/introspect

認証サーバーの イントロスペクションURL と、アプリケーションが認証サーバーへの認証に使用する client-id / client-secret を指定する必要があります。+ エクステンションは、この情報を使ってトークンを検証し、トークンに関連する情報を復元します。

すべての設定プ ロパテ ィ については、 こ のガ イ ド の最後にあ る 設定リファレンス セ ク シ ョ ン を参照 し て く だ さ い。

アプリケーションの実行

これで、アプリケーションを実行する準備が整いました。次を実行してください:

コマンドラインインタフェース
quarkus dev
Maven
./mvnw quarkus:dev
Gradle
./gradlew --console=plain quarkusDev

REST エンドポイントが実行されているので、curl のようなコマンドラインツールを使ってアクセスできます:

$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isSecure: false, authScheme: null

リクエストでトークンを提供していないため、エンドポイントが見ているセキュリティー状態があるとは期待できず、レスポンスもそれと一致しています:

  • ユーザー名は匿名です

  • isSecure は https が使用されていないため false です

  • authScheme はnullです

エンドポイントのセキュア化

では実際に何かをセキュア化してみましょう。下記の新しいエンドポイントメソッド helloRolesAllowed を見てみましょう:

package org.acme.security.oauth2;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
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 jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;

@Path("/secured")
@ApplicationScoped
public class TokenSecuredResource {

    @GET()
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        Principal caller =  ctx.getUserPrincipal();
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply;
    }

    @GET()
    @Path("roles-allowed") (1)
    @RolesAllowed({"Echoer", "Subscriber"}) (2)
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        Principal caller =  ctx.getUserPrincipal();
        String name = caller == null ? "anonymous" : caller.getName();
        String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme());
        return helloReply;
    }
}
1 この新しいエンドポイントは /secured/roles-allowed に配置されます
2 @RolesAllowed は指定されたエンドポイントに、呼び出し側が “Echoer” または “Subscriber” のいずれかのロールを割り当てている場合にアクセス可能であることを示します。

TokenSecuredResource に追加後、 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo を実行し、新しいエンドポイントへのアクセスを試してください。出力は次のようになるはずです:

$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
*   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Not authorized

素晴らしいことに、リクエストでOAuth2トークンを提供していないので、エンドポイントにアクセスできないはずですが、そうではありませんでした。代わりに、HTTP 401 Unauthorized エラーが発生しました。エンドポイントにアクセスするためには、有効な OAuth2 トークンを取得して渡す必要があります。1) Elytron Security OAuth2 エクステンションにトークンの検証方法を設定する、2) 適切なクレームを含む一致するトークンを生成する。

トークンの生成

標準的なOAuth2認証サーバー(例えば Keycloak )から、トークンのエンドポイントを使用してトークンを取得する必要があります。

以下に、 client_credential フローに対するこのような呼び出しのcurlの例を示します:

curl -X POST "http://oauth-server/token?grant_type=client_credentials" \
-H  "Accept: application/json" -H  "Authorization: Basic Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ="

これは以下のような応答をする筈です.

{"access_token":"60acf56d-9daf-49ba-b3be-7a423d9c7288","token_type":"bearer","expires_in":1799,"scope":"READER"}

最後に、/secured/roles-allowed へのセキュアなリクエストを行います

これを使って /secured/roles-allowed エンドポイントにセキュアなリクエストをしてみましょう

$ curl -H "Authorization: Bearer 60acf56d-9daf-49ba-b3be-7a423d9c7288" http://127.0.0.1:8080/secured/roles-allowed; echo
hello + client_id isSecure: false, authScheme: OAuth2

成功です! これで以下が得られます:

  • client_id の匿名でない呼出元名

  • OAuth2 の認証スキーム

ロールマッピング

ロールは、イントロスペクションのエンドポイントレスポンスのクレームの1つからマッピングされます。デフォルトでは、 scope クレームです。ロールはクレームをスペース区切りで分割して取得します。クレームが配列の場合、分割は行われず、ロールは配列から取得されます。

ロールに使用するクレームの名前は、 quarkus.oauth2.role-claim プロパティーでカスタマイズできます。

アプリケーションをパッケージ化して実行する

いつものように、アプリケーションは以下の方法でパッケージ化できます:

コマンドラインインタフェース
quarkus build
Maven
./mvnw install
Gradle
./gradlew build

そして、 java -jar target/quarkus-app/quarkus-run.jar を使って実行します:

[INFO] Scanning for projects...
...
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO  [io.quarkus] (main) Quarkus 3.10.1 started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO  [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]

ネイティブ実行可能ファイルを ./mvnw clean package -Pnative で生成することもできます:

コマンドラインインタフェース
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.native.enabled=true
[INFO] Scanning for projects...
...
[security-oauth2-quickstart-runner:25602]     universe:     493.17 ms
[security-oauth2-quickstart-runner:25602]      (parse):     660.41 ms
[security-oauth2-quickstart-runner:25602]     (inline):   1,431.10 ms
[security-oauth2-quickstart-runner:25602]    (compile):   7,301.78 ms
[security-oauth2-quickstart-runner:25602]      compile:  10,542.16 ms
[security-oauth2-quickstart-runner:25602]        image:   2,797.62 ms
[security-oauth2-quickstart-runner:25602]        write:     988.24 ms
[security-oauth2-quickstart-runner:25602]      [total]:  43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  51.500 s
[INFO] Finished at: 2019-06-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------

$ ./target/security-oauth2-quickstart-runner
2019-03-28 14:31:37,315 INFO  [io.quarkus] (main) Quarkus 0.20.0 started in 0.006s. Listening on: http://[::]:8080
2019-03-28 14:31:37,316 INFO  [io.quarkus] (main) Installed features: [cdi, rest, rest-jackson, security, security-oauth2]

統合テスト

統合テストに本物の OAuth2 認可サーバーを使いたくない場合は、テストに プロパティベースのセキュリティ エクステンションを使うか、Wiremock を使って認可サーバーをモックすることができます。

まず、Wiremockをテストの依存関係として追加する必要があります。Mavenプロジェクトの場合は以下のようになります:

pom.xml
<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> (1)
</dependency>
1 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。
build.gradle
testImplementation("org.wiremock:wiremock:${wiremock.version}") (1)
1 適切なWiremockバージョンを使用してください。利用可能なすべてのバージョンは、 こちら をご覧ください。

Quarkusのテストを実行する前にサービスを開始する必要がある場合、Quarkusのテストでは、 @io.quarkus.test.common.QuarkusTestResource アノテーションを利用して、サービスを開始できる io.quarkus.test.common.QuarkusTestResourceLifecycleManager を指定し、Quarkusが使用する設定値を提供します。

@QuarkusTestResource の詳細については、 ドキュメントの当該箇所 を参照してください。

このように QuarkusTestResourceLifecycleManager の実装である MockAuthorizationServerTestResource を作成してみましょう:

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

import java.util.Collections;
import java.util.Map;

public class MockAuthorizationServerTestResource implements QuarkusTestResourceLifecycleManager {  (1)

    private WireMockServer wireMockServer;

    @Override
    public Map<String, String> start() {
        wireMockServer = new WireMockServer();
        wireMockServer.start(); (2)

        // define the mock for the introspect endpoint
        WireMock.stubFor(WireMock.post("/introspect").willReturn(WireMock.aResponse() (3)
                .withBody(
                        "{\"active\":true,\"scope\":\"Echoer\",\"username\":null,\"iat\":1562315654,\"exp\":1562317454,\"expires_in\":1458,\"client_id\":\"my_client_id\"}")));


        return Collections.singletonMap("quarkus.oauth2.introspection-url", wireMockServer.baseUrl() + "/introspect"); (4)
    }

    @Override
    public void stop() {
        if (null != wireMockServer) {
            wireMockServer.stop();  (5)
        }
    }
}
1 start メソッドは、テストを実行する前にQuarkusによって呼び出され、テスト実行中に適用される設定プロパティーの Map を返します。
2 Wiremockを起動します。
3 OAuth2のイントロスペクトレスポンスを返すことで、 /introspect への呼び出しをスタブ化するように Wiremock を設定します。この行をカスタマイズして、アプリケーションに必要なものを返すようにする必要があります (ロールはスコープから派生しているので、少なくともスコープのプロパティーは必要です)。
4 start メソッドはテストに適用される設定を返すので、OAuth2 エクステンションで使用する introspect エンドポイントの URL を制御する quarkus.oauth2.introspection-url プロパティーを設定します。
5 すべてのテストが終了したら、Wiremockをシャットダウンします。

この QuarkusTestResourceLifecycleManager を使用するには、テストクラスに @QuarkusTestResource(MockAuthorizationServerTestResource.class) のようなアノテーションを付ける必要があります。

以下は、 MockAuthorizationServerTestResource を使用したテストの例です。

@QuarkusTest
@QuarkusTestResource(MockAuthorizationServerTestResource.class) (1)
class TokenSecuredResourceTest {
    // use whatever token you want as the mock OAuth server will accept all tokens
    private static final String BEARER_TOKEN = "337aab0f-b547-489b-9dbd-a54dc7bdf20d"; (2)

    @Test
    void testPermitAll() {
        RestAssured.given()
                .when()
                .header("Authorization", "Bearer: " + BEARER_TOKEN) (3)
                .get("/secured/permit-all")
                .then()
                .statusCode(200)
                .body(containsString("hello"));
    }

    @Test
    void testRolesAllowed() {
        RestAssured.given()
                .when()
                .header("Authorization", "Bearer: " + BEARER_TOKEN)
                .get("/secured/roles-allowed")
                .then()
                .statusCode(200)
                .body(containsString("hello"));
    }
}
1 以前に作成した MockAuthorizationServerTestResource をQuarkusのテストリソースとして使用します。
2 任意のトークンを定義してください。OAuth2のモック認可サーバーでは検証されません。
3 このトークンを Authorization ヘッダ内で使用して、OAuth2 認証を開始します。

@QuarkusTestResourceTokenSecuredResourceTest だけでなく、すべてのテストに適用されます。

設定リファレンス

ビルド時に固定される構成プロパティ - 他のすべての構成プロパティは実行時にオーバーライド可能

Configuration property

デフォルト

Determine if the OAuth2 extension is enabled. Enabled by default if you include the elytron-security-oauth2 dependency, so this would be used to disable it.

Environment variable: QUARKUS_OAUTH2_ENABLED

Show more

boolean

true

The claim that is used in the introspection endpoint response to load the roles.

Environment variable: QUARKUS_OAUTH2_ROLE_CLAIM

Show more

string

scope

The OAuth2 client id used to validate the token. Mandatory if the extension is enabled.

Environment variable: QUARKUS_OAUTH2_CLIENT_ID

Show more

string

The OAuth2 client secret used to validate the token. Mandatory if the extension is enabled.

Environment variable: QUARKUS_OAUTH2_CLIENT_SECRET

Show more

string

The OAuth2 introspection endpoint URL used to validate the token and gather the authentication claims. Mandatory if the extension is enabled.

Environment variable: QUARKUS_OAUTH2_INTROSPECTION_URL

Show more

string

The OAuth2 server certificate file. Warning: this is not supported in native mode where the certificate must be included in the truststore used during the native image generation, see Using SSL With Native Executables.

Environment variable: QUARKUS_OAUTH2_CA_CERT_FILE

Show more

string

関連コンテンツ