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

Using OAuth2 RBAC

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

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

このエクステンションは、不透明なベアラートークンを使用してイントロスペクションエンドポイントを呼び出して検証するための軽量なサポートを提供します。

If the OAuth2 Authentication server provides JWT Bearer Tokens then you should consider using either OpenID Connect or SmallRye JWT extensions instead. OpenID Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow, please read Using OpenID Connect to Protect Web Applications guide for more information.

この技術は、{extension-status}と考えられています。

For a full list of possible statuses, check our FAQ entry.

ソリューション

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

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

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

Mavenプロジェクトの作成

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

CLI
quarkus create app org.acme:security-oauth2-quickstart \
    --extension=resteasy-reactive-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:2.11.1.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-oauth2-quickstart \
    -Dextensions="resteasy-reactive-jackson,security-oauth2" \
    -DnoCode
cd security-oauth2-quickstart

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

This command generates a project and imports the elytron-security-oauth2 extension, which includes the OAuth2 opaque token support.

If you don’t want to use the Maven plugin, you can just include the dependency in your build file:

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

JAX-RSリソースを調べる

Create the src/main/java/org/acme/security/oauth2/TokenSecuredResource.java file with the following content:

package org.acme.security.oauth2;

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

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

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

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

We will use the JSR 250 common security annotations, they are described in the Using Security guide.

package org.acme.security.oauth2;

import java.security.Principal;

import javax.annotation.security.PermitAll;
import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.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 ここでは、JAX-RS SecurityContext を注入して、呼出のセキュリティー状態を検査します。
3 Here we obtain the current request user/caller Principal. For an unsecured call this will be null, so we build the username by checking caller against 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 を指定する必要があります。+ エクステンションはこの情報を使ってトークンを検証し、関連する情報を復元します。

For all configuration properties, see the Configuration reference section at the end of this guide.

アプリケーションの実行

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

CLI
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

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

  • username is anonymous

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

  • authScheme はnullです。

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

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

package org.acme.security.oauth2;

import java.security.Principal;

import javax.annotation.security.PermitAll;
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 javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.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 indicates that the given endpoint is accessible by a caller if they have either an "Echoer" or a "Subscriber" role assigned.

あなたの 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

Excellent, we have not provided any OAuth2 token in the request, so we should not be able to access the endpoint, and we were not. Instead, we received an HTTP 401 Unauthorized error. We need to obtain and pass in a valid OAuth2 token to access that endpoint. There are two steps to this, 1) configuring our Elytron Security OAuth2 extension with information on how to validate the token, and 2) generating a matching token with the appropriate claims.

トークンの生成

トークンエンドポイントを使用して、標準的な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 プロパティーを使用して、ロールに使用するクレームの名前をカスタマイズすることができます。

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

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

CLI
quarkus build
Maven
./mvnw clean package
Gradle
./gradlew build

And executed using 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 2.11.1.Final started in 0.796s. Listening on: http://[::]:8080
2019-03-28 14:27:48,841 INFO  [io.quarkus] (main) Installed features: [cdi, resteasy-reactive, resteasy-reactive-jackson, security, security-oauth2]

You can also generate the native executable with:

CLI
quarkus build --native
Maven
./mvnw package -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native
[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, resteasy-reactive, resteasy-reactive-jackson, security, security-oauth2]

統合テスト

If you don’t want to use a real OAuth2 authorization server for your integration tests, you can use the Properties based security extension for your test, or mock an authorization server using Wiremock.

First, Wiremock needs to be added as a test dependency. For a Maven project that would happen like so:

pom.xml
<dependency>
    <groupId>com.github.tomakehurst</groupId>
    <artifactId>wiremock-jre8</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> (1)
</dependency>
1 適切なWiremockのバージョンを使用してください。利用可能なすべてのバージョンは ここ で見つけることができます。
build.gradle
testImplementation("com.github.tomakehurst:wiremock-jre8:${wiremock.version}") (1)
1 適切なWiremockのバージョンを使用してください。利用可能なすべてのバージョンは ここ で見つけることができます。

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

For more details about @QuarkusTestResource refer to this part of the documentation.

このように 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

boolean

true

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

Environment variable: QUARKUS_OAUTH2_ROLE_CLAIM

string

scope

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

Environment variable: QUARKUS_OAUTH2_CLIENT_ID

string

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

Environment variable: QUARKUS_OAUTH2_CLIENT_SECRET

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

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

string