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

JWT RBAC の使用

このガイドでは、Quarkusアプリケーションが SmallRye JWT を利用して、 JSON Web Token を検証し、MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken として表現し、Bearer Token Authorizationと Role-Based Access Control を使用してQuarkus HTTPエンドポイントへのセキュアなアクセスを提供する方法について説明します。

Quarkus OpenID Connect quarkus-oidc エクステンションは、ベアラートークン認証にも対応しており、 smallrye-jwt を使用して、ベアラートークンを JsonWebToken として表します。 詳細については、 OIDCベアラートークン認証 ガイドを参照してください。 Quarkus アプリケーションで OIDC 認可コードフローを使用してユーザーを認証する必要がある場合は、OpenID Connect エクステンションを使用する必要があります。 詳細については、 Webアプリケーションを保護するためのOIDCコードフロー メカニズムを参照してください。

前提条件

このガイドを完成させるには、以下が必要です:

  • 約15分

  • IDE

  • JDK 17+がインストールされ、 JAVA_HOME が適切に設定されていること

  • Apache Maven 3.9.6

  • 使用したい場合は、 Quarkus CLI

  • ネイティブ実行可能ファイルをビルドしたい場合、MandrelまたはGraalVM(あるいはネイティブなコンテナビルドを使用する場合はDocker)をインストールし、 適切に設定していること

クイックスタート

ソリューション

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

Git リポジトリーのクローンを作成: git clonehttps://github.com/quarkusio/quarkus-quickstarts.git、または アーカイブ をダウンロードします。

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

Maven プロジェクトの作成

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

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

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

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

Maven
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.3:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=security-jwt-quickstart \
    -Dextensions='resteasy-reactive-jackson,smallrye-jwt,smallrye-jwt-build' \
    -DnoCode
cd security-jwt-quickstart

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

Windowsユーザーの場合:

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

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

このコマンドは、RESTエンドポイントを持つMavenプロジェクトを生成し、MicroProfile JWT RBAC サポートを含む smallrye-jwt エクステンションをインポートします。

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

コマンドラインインタフェース
quarkus extension add smallrye-jwt,smallrye-jwt-build
Maven
./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
Gradle
./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'

これにより、 pom.xml に以下が追加されます:

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-jwt-build")

Jakarta REST リソースの調査

RESTエンドポイントを src/main/java/org/acme/security/jwt/TokenSecuredResource.java に以下の内容で作成します。

REST エンドポイント V1
package org.acme.security.jwt;

import java.security.Principal;

import jakarta.annotation.security.PermitAll;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)

    @GET()
    @Path("permit-all")
    @PermitAll (2)
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx); (3)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) { (4)
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { (5)
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName(); (6)
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); (7)
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 ここでは、現在認証されているトークンに関連付けられたクレームへのアクセスを提供する java.security.Principal インターフェイスのエクステンションである JsonWebToken インターフェイスを挿入します。
2 @PermitAllは、Jakarta共通セキュリティアノテーションで、認証されているかどうかにかかわらず、指定されたエンドポイントにはどの呼び出し元からもアクセスできることを示します。
3 ここでは、Jakarta REST SecurityContext を注入して呼び出しのセキュリティ状態を検査し、 getResponseString() 関数を使用して応答文字列を入力しています。
4 ここでは、リクエストのユーザー/呼び出し元の Principal を null と照合して、呼び出しが安全でないかどうかを確認します。
5 ここでは、JsonWebToken が現在の Principal を表しているため、Principal と JsonWebToken が同じ名前であることを確認します。
6 ここで、Principal の名前を取得します。
7 構築した応答では、呼び出し元の名前、リクエスト SecurityContextisSecure()getAuthenticationScheme() の状態、null 以外の JsonWebToken が挿入されたかどうかを利用します。

アプリケーションの実行

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

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

次のような出力が得られるはずです:

quarkus:dev 出力
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< org.acme:security-jwt-quickstart >-----------------------
[INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
...
Listening for transport dt_socket at address: 5005
2020-07-15 16:09:50,883 INFO  [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080
2020-07-15 16:09:50,885 INFO  [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-07-15 16:09:50,885 INFO  [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, resteasy-reactive, resteasy-reactive-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]

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

/secured/permit-all に対するcurl コマンド
$ curl http://127.0.0.1:8080/secured/permit-all; echo
hello + anonymous, isHttps: false, authScheme: null, hasJWT: false

このリクエストではJWTを設定していませんので、エンドポイントから見たセキュリティーステートがあるとは期待できず、レスポンスもそれに沿ったものとなっています。

  • ユーザー名は anonymous (匿名) です。

  • isHttps は https を使用しないので false です。

  • authScheme は null です。

  • hasJWTは false です。

Ctrl-Cを使用してQuarkusサーバーを停止します。

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

REST エンドポイント V2
package org.acme.security.jwt;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)

    @GET
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx);
    }

    @GET
    @Path("roles-allowed") (2)
    @RolesAllowed({ "User", "Admin" }) (3)
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); (4)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) {
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName();
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 ここでは、JsonWebToken を挿入します。
2 この新しいエンドポイントは /secured/roles-allowed に存在します。
3 @RolesAllowed は、Jakarta共通セキュリティアノテーションで、指定されたエンドポイントは、呼び出し元が "User "または "Admin "ロールを割り当てている場合にアクセス可能であることを示します。
4 ここでは、hello メソッドと同じ方法で応答を作成しますが、挿入された JsonWebToken を直接呼び出すことで JWT birthdate クレームの値を追加します。

TokenSecuredResource にこの追加を行った後 、 ./mvnw compile quarkus:dev コマンドを再実行し、次のように curl -v http://127.0.0.1:8080/secured/roles-allowed ; echo を使用して、新しいエンドポイントへのアクセスを試行してみてください。

出力は以下のようになるはずです:

/secured/roles-allowed の curl コマンド
$ 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

素晴らしいですね。リクエストに JWT トークンを提供していないため、エンドポイントにはアクセスできないはずであり、実際にアクセスできませんでした。代わりに HTTP 401 Unauthorized エラーを受け取りました。エンドポイントにアクセスするためには、有効な JWT を取得して渡す必要があります。これには 2 つのステップがあります。1) SmallRye JWT エクステンションに JWT の検証方法に関する情報を設定し、2) 適切なクレームを持つ適合する JWT を生成します。

SmallRye JWT エクステンションセキュリティー情報の設定

次の内容で security-jwt-quickstart/src/main/resources/application.properties を作成します。

TokenSecuredResource の application.properties
mp.jwt.verify.publickey.location=publicKey.pem (1)
mp.jwt.verify.issuer=https://example.com/issuer (2)

quarkus.native.resources.includes=publicKey.pem (3)
1 We are setting public key location to point to a classpath publicKey.pem location. We will add this key in part B, 公開鍵の追加.
2 発行者を URL 文字列 https://example.com/issuer に設定しています。
3 ネイティブ実行可能ファイルのリソースとして公開鍵を含めています。

公開鍵の追加

JWT 仕様 は、使用できる JWT のさまざまなレベルのセキュリティーを定義しています。MicroProfile JWT RBAC 仕様では、RSA-256 署名アルゴリズムで署名された JWT が必要です。これには、RSA 公開鍵ペアが必要です。REST エンドポイントサーバー側では、リクエストとともに送信された JWT を検証するために使用する RSA 公開鍵の場所を設定する必要があります。以前に設定され た mp.jwt.verify.publickey.location=publicKey.pem 設定は、公開鍵がクラスパスで publicKey.pem として使用可能であることを想定しています。これを行うには、次のコンテンツを security-jwt-quickstart/src/main/resources/publicKey.pem ファイルにコピーします。

RSA 公開鍵 PEM コンテンツ
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----

JWT の生成

多くの場合、 Keycloak のようなIDマネージャからJWTを取得しますが、このクイックスタートでは、 smallrye-jwt が提供するJWT生成APIを使用して独自に生成することにします。詳細は、SmallRye JWTでJWTトークンを生成 を参照してください。

次のリストからコードを取得し、security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java に配置します。

GenerateToken メインドライバークラス
package org.acme.security.jwt;

import java.util.Arrays;
import java.util.HashSet;

import org.eclipse.microprofile.jwt.Claims;

import io.smallrye.jwt.build.Jwt;

public class GenerateToken {
    /**
     * Generate JWT token
     */
    public static void main(String[] args) {
        String token =
           Jwt.issuer("https://example.com/issuer") (1)
             .upn("jdoe@quarkus.io") (2)
             .groups(new HashSet<>(Arrays.asList("User", "Admin"))) (3)
             .claim(Claims.birthdate.name(), "2001-07-13") (4)
           .sign();
        System.out.println(token);
    }
}
1 iss クレームは JWT の発行者です。トークンが有効であると認められるためには、これがサーバー側の mp.jwt.verify.issuer と一致する必要があります。
2 upn クレームは、MicroProfile JWT RBAC 仕様で、コンテナーセキュリティー API を介して表示される Principal に使用する優先クレームとして定義されています。
3 group クレームは、JWT ベアラーに関連付けられたグループとトップレベルのロールを提供します。
4 birthday クレームです。機密性の高いクレームと見なすことができるため、クレームの暗号化を検討したくなるかもしれません。 SmallRye JWT を使用した JWT トークンの生成 を参照してください。

このコードが機能するには、TokenSecuredResource アプリケーションにある公開鍵に対応する RSA 秘密鍵のコンテンツが必要であることに注意してください。次の PEM コンテンツを取得し、security-jwt-quickstart/src/test/resources/privateKey.pem に配置します。

RSA 秘密鍵 PEM コンテンツ
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----

smallrye.jwt.sign.key.location プロパティーを使用して、この秘密署名鍵を指します。

OpenSSL を使用した鍵の生成

OpenSSL コマンドラインツールを使用して、公開鍵と秘密鍵のペアを生成することもできます。

鍵を生成するための openssl コマンド
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem

秘密鍵を生成して PKCS#8 形式に変換するには、追加の手順が必要です。

秘密鍵を変換するための openssl コマンド
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem

このクイックスタートで使用される鍵の代わりに、生成された鍵のペアを使用できます。

これで、TokenSecuredResource エンドポイントで使用する JWT を生成できます。これを行うには、次のコマンドを実行します。

JWT 生成出力のサンプル
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem

eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg

JWT 文字列は Base64 URL でエンコードされた文字列で、'.' で区切られた 3 つのパートで構成されています。最初のパートは JWT ヘッダー、2 つ目のパートは JWT クレーム、3 つ目のパートは JWT シグネチャーです。

/secured/roles-allowed へのセキュリティーで保護されたアクセス

これを使用して、/secured/roles-allowed エンドポイントにセキュリティーで保護されたリクエストを作成しましょう。Quarkus サーバーが引き続き開発モードで実行されていることを確認してから、次のコマンドを実行し、前の手順で生成された自分の JWT バージョンを使用していることを確認します。

curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed; echo
JWT を含む /secured/roles-allowed の curl コマンド
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13

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

  • jdoe@quarkus.io という非匿名の発信者名

  • ベアラーの認証スキーム

  • 非 null の JsonWebToken

  • birthdate クレームの値

JsonWebToken とクレーム挿入の使用

さて、安全なRESTエンドポイントにアクセスするためにJWTを生成できるようになったので、 JsonWebToken インターフェースとJWTクレームを使ってさらに何ができるかを見てみましょう。 org.eclipse.microprofile.jwt.JsonWebToken インターフェースは java.security.Principal インターフェースを拡張したもので、実際には前回使用した jakarta.ws.rs.core.SecurityContext#getUserPrincipal() 呼び出しによって返されるオブジェクトの型です。つまり、CDIを使用せず、RESTコンテナ SecurityContext にアクセスできるコードは、 SecurityContext#getUserPrincipal() をキャストすることで、呼び出し元の JsonWebToken インターフェースを取得することができます。

JsonWebToken インターフェイスは、基盤となる JWT のクレームにアクセスするためのメソッドを定義します。これは、JWT に存在する可能性のある MicroProfile JWT RBAC 仕様および任意のクレームで必要な一般的クレームのアクセッサを提供します。。

すべての JWT クレームを挿入することもできます。TokenSecuredResource を別のエンドポイント /secured/roles-allowed-admin で拡張してみましょう。これは、(JsonWebToken から取得するのではなく) 挿入された birthdate クレームを使用します。

package org.acme.security.jwt;

import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
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;

import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;

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

    @Inject
    JsonWebToken jwt; (1)
    @Inject
    @Claim(standard = Claims.birthdate)
    String birthdate; (2)

    @GET
    @Path("permit-all")
    @PermitAll
    @Produces(MediaType.TEXT_PLAIN)
    public String hello(@Context SecurityContext ctx) {
        return getResponseString(ctx);
    }

    @GET
    @Path("roles-allowed")
    @RolesAllowed({ "User", "Admin" })
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowed(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
    }

    @GET
    @Path("roles-allowed-admin")
    @RolesAllowed("Admin")
    @Produces(MediaType.TEXT_PLAIN)
    public String helloRolesAllowedAdmin(@Context SecurityContext ctx) {
        return getResponseString(ctx) + ", birthdate: " + birthdate; (3)
    }

    private String getResponseString(SecurityContext ctx) {
        String name;
        if (ctx.getUserPrincipal() == null) {
            name = "anonymous";
        } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
            throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
        } else {
            name = ctx.getUserPrincipal().getName();
        }
        return String.format("hello + %s,"
            + " isHttps: %s,"
            + " authScheme: %s,"
            + " hasJWT: %s",
            name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
    }

    private boolean hasJwt() {
        return jwt.getClaimNames() != null;
    }
}
1 ここでは、JsonWebToken を挿入します。
2 ここでは、birthday クレームを String として挿入します。これが @RequestScoped スコープが必要になった理由です。
3 ここでは、挿入された birthday クレームを使用して、最終的な応答を作成します。

次に、トークンを再度生成して実行します。

curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed-admin; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
hello + jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13

アプリケーションのパッケージ化と実行

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

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

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

ランナー jar の例
$ java -jar target/quarkus-app/quarkus-run.jar
2019-03-28 14:27:48,839 INFO  [io.quarkus] (main) Quarkus 3.8.3 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, smallrye-jwt]

次のようにネイティブ実行可能ファイルを生成することもできます。

コマンドラインインタフェース
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native
ネイティブ実行可能ファイル例
[INFO] Scanning for projects...
...
[security-jwt-quickstart-runner:25602]     universe:     493.17 ms
[security-jwt-quickstart-runner:25602]      (parse):     660.41 ms
[security-jwt-quickstart-runner:25602]     (inline):   1,431.10 ms
[security-jwt-quickstart-runner:25602]    (compile):   7,301.78 ms
[security-jwt-quickstart-runner:25602]      compile:  10,542.16 ms
[security-jwt-quickstart-runner:25602]        image:   2,797.62 ms
[security-jwt-quickstart-runner:25602]        write:     988.24 ms
[security-jwt-quickstart-runner:25602]      [total]:  43,778.16 ms
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  51.500 s
[INFO] Finished at: 2019-03-28T14:30:56-07:00
[INFO] ------------------------------------------------------------------------

$ ./target/security-jwt-quickstart-runner
2019-03-28 14:31:37,315 INFO  [io.quarkus] (main) Quarkus 0.12.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, smallrye-jwt]

ソリューションの探索

The solution repository located in the security-jwt-quickstart directory contains all the versions we have worked through in this quickstart guide as well as some additional endpoints that illustrate subresources with injection of JsonWebTokens and their claims into those using the CDI APIs. We suggest that you check out the quickstart solutions and explore the security-jwt-quickstart directory to learn more about the SmallRye JWT extension features.

リファレンスガイド

サポートされている注入スコープ

org.eclipse.microprofile.jwt.JsonWebToken が注入される場合、@ApplicationScoped@Singleton、および @RequestScoped の外部 Bean 注入スコープがすべてサポートされます。また、現在のトークンが示されていることを確認するために、JsonWebToken@RequestScoped スコープが適用されます。

ただし、個々のトークンクレームが String などの単純な型として挿入される場合は、@RequestScoped を使用する必要があります。以下はその例です。

package org.acme.security.jwt;

import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;

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

    @Inject
    @Claim(standard = Claims.birthdate)
    String birthdate;
}

注入された JsonWebToken を使用して個々のクレームにアクセスすることもできます。その場合、@RequestScoped を設定する必要はありません。

詳細については、 MP JWT CDI 注入要件 を参照してください。

サポートされている公開鍵形式

公開鍵には、優先順位に従って指定された次のいずれかの形式が適用さされます。

  • Public Key Cryptography Standards #8 (PKCS#8) PEM

  • JSON Web Key (JWK)

  • JSON Web Key Set (JWKS)

  • JSON Web Key (JWK) Base64 URL エンコード

  • JSON Web Key Set (JWKS) Base64 URL エンコード

検証鍵の処理

非対称 RSA または楕円曲線 (EC) 鍵を使用してトークンの署名を検証する必要がある場合は、mp.jwt.verify.publickey.location プロパティーを使用してローカルまたはリモートの検証鍵を参照します。

mp.jwt.verify.publickey.algorithm を使用して検証アルゴリズムをカスタマイズします (デフォルトは RS256)。たとえば、EC 鍵を操作する場合は ES256 に設定します。

対称秘密鍵を使用してトークン署名を検証する必要がある場合は、JSON Web 鍵 (JWK) または JSON Web 鍵セット (JWK セット) 形式のいずれかを使用して、この秘密鍵を表す必要があります。以下はその例です。

{
 "keys": [
   {
     "kty":"oct",
     "kid":"secretKey",
     "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
   }
 ]
}

この秘密鍵 JWK も、smallrye.jwt.verify.key.location で参照する必要があります。smallrye.jwt.verify.algorithmHS256/HS384/HS512 に設定する必要があります。

JWTParser を使用した JsonWebToken の解析と検証

JWT トークンを挿入できない場合、たとえば、サービスリクエストペイロードに埋め込まれていたり、サービスエンドポイントが別の経路でトークンを取得する場合、JWTParser を使用できます。

import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
...
@Inject JWTParser parser;

String token = getTokenFromOidcServer();

// Parse and verify the token
JsonWebToken jwt = parser.parse(token);

これを使用して、トークンの検証または復号化の方法をカスタマイズすることもできます。たとえば、ローカルの SecretKey を指定できます。

package org.acme.security.jwt;

import io.smallrye.jwt.auth.principal.ParseException;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.build.Jwt;

@Path("/secured")
public class SecuredResource {
    private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";

    @Inject
    JWTParser parser;

    @GET
    @Produces("text/plain")
    public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException {
        if (jwtCookie == null) {
            // Create a JWT token signed using the 'HS256' algorithm
            String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET);
            // or create a JWT token encrypted using the 'A256KW' algorithm
            // Jwt.upn("alice").encryptWithSecret(secret);
            return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build();
        } else {
            // All mp.jwt and smallrye.jwt properties are still effective, only the verification key is customized.
            JsonWebToken jwt = parser.verify(jwtCookie, SECRET);
            // or jwt = parser.decrypt(jwtCookie, secret);
            return Response.ok(jwt.getName()).build();
        }
    }
}

Please also see the How to Add SmallRye JWT directly section about using JWTParser without the HTTP support provided by quarkus-smallrye-jwt.

トークン復号化

アプリケーションが暗号化されたクレームや暗号化・内部署名付きクレームを持つトークンを受け入れる必要がある場合は、必要な作業は、復号化キーを指す smallrye.jwt.decrypt.key.location を設定することだけです。

この鍵プロパティのみが設定されている場合、受信トークンには暗号化クレームのみが含まれることが期待されます。 mp.jwt.verify.publickey または mp.jwt.verify.publickey.location のいずれかの検証プロパティも設定されている場合、受信トークンには暗号化・内部署名付きトークンが含まれることが期待されます。

SmallRye JWT を使用した JWT トークンの生成 を参照し、暗号化または内部署名されてから暗号化されたトークンをすぐに生成する方法を確認してください。

カスタムファクトリー

io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory は、デフォルトで JWT トークンを解析および検証し、それらを JsonWebToken プリンシパルに変換するために使用されます。設定セクションにリストされている MP JWT および smallrye-jwt プロパティーを使用して、JWT トークンを検証およびカスタマイズします。

たとえば、ファイアウォールによってすでに検証されているトークンの再検証を回避するために独自のファクトリーを提供する必要がある場合は、META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory リソースを提供すして ServiceLoader メカニズムを使用するか、単純に次のような Alternative CDI Bean 実装を持つことができます。

import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory;
import io.smallrye.jwt.auth.principal.ParseException;

@ApplicationScoped
@Alternative
@Priority(1)
public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory {

    @Override
    public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
        try {
            // Token has already been verified, parse the token claims only
            String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
            return new DefaultJWTCallerPrincipal(JwtClaims.parse(json));
        } catch (InvalidJwtException ex) {
            throw new ParseException(ex.getMessage());
        }
    }
}

ブロッキング呼出

quarkus-smallrye-jwt エクステンションでは、現在リアクティブではない SmallRye JWT ライブラリを使用します。

`quarkus-smallrye-jwt`がリアクティブなQuarkusセキュリティ・アーキテクチャの一部として動作する観点からこれが意味することは 、 SmallRye JWT 検証または復号化コードに入るIOスレッドが、以下のいずれかのケースでブロックされる可能性があるということです:

  • デフォルトのキーリゾルバーが、OIDCエンドポイントへのリモートコールを伴うキーを含む JsonWebKey セットを更新します

  • Custom key resolver such as AWS Application Load Balancer (ALB) key resolver, resolves the keys against the AWS ALB key endpoint using the current token’s key identifier header value

In such cases, if the connections are slow, for example, it may take more than 3 seconds to get a response from the key endpoint, the current event loop thread will most likely block.

これを防ぐには、 quarkus.smallrye-jwt.blocking-authentication=true を設定します。

トークンの伝播

Please see the Token Propagation section about the Bearer access token propagation to the downstream services.

テスト

Wiremock

もし、 mp.jwt.verify.publickey.location が HTTPS または HTTP ベースの JsonWebKey (JWK) セットを指すように設定している場合は、 OpenID Connect Bearer Token結合テスト Wiremock のセクションで説明したのと同じアプローチが使えますが、代わりに MP JWT 設定プロパティを使うように application.properties を変更するだけです:

# keycloak.url is set by OidcWiremockTestResource
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

Keycloak

Keycloakと連携し、 mp.jwt.verify.publickey.location 、HTTPSまたはHTTPベースのJsonWebKey (JWK)セットを指すように設定している場合は、 OpenID Connect Bearer Token結合テスト Keycloakセクションで説明したのと同じアプローチを使用できますが、代わりにMP JWT設定プロパティを使用するように application.properties を変更するだけです:

# keycloak.url is set by DevServices for Keycloak
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

Keycloakが発行したトークンは、レルムエンドポイントアドレスが iss (発行者)クレームに設定されていることに注目してください。

QuarkusアプリケーションがDockerコンテナで実行されている場合、DevServices for Keycloakによって起動されたKeycloak dockerコンテナとネットワークインターフェースを共有し、QuarkusアプリケーションとKeycloakは内部の共有Dockerネットワークを通じて互いに通信することができます。

そのような場合は、代わりに以下のような設定にしてください。

# keycloak.url is set by DevServices for Keycloak,
# Quarkus will access it via an internal shared docker network interface.
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs

# Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url}

ローカル公開鍵

OpenID Connect Bearer Token結合テスト Local Public Key のセクションで説明したのと同じアプローチを使用できますが、代わりに MP JWT 設定プロパティを使用するように application.properties を変更するだけです:

mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
# set it to the issuer value which is used to generate the tokens
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus

# required to sign the tokens
smallrye.jwt.sign.key.location=privateKey.pem

TestSecurity アノテーション

以下の依存関係を追加します。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-test-security-jwt</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("io.quarkus:quarkus-test-security-jwt")

次のようなテストコードを作成します。

import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.jwt.Claim;
import io.quarkus.test.security.jwt.JwtSecurity;
import io.restassured.RestAssured;

@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {

    @Test
    @TestSecurity(user = "userJwt", roles = "viewer")
    public void testJwt() {
        RestAssured.when().get("test-security-jwt").then()
                .body(is("userJwt:viewer"));
    }

    @Test
    @TestSecurity(user = "userJwt", roles = "viewer")
    @JwtSecurity(claims = {
            @Claim(key = "email", value = "user@gmail.com")
    })
    public void testJwtWithClaims() {
        RestAssured.when().get("test-security-jwt-claims").then()
                .body(is("userJwt:viewer:user@gmail.com"));
    }

}

ここで、ProtectedResource クラスは次のようなものです:

@Path("/web-app")
@Authenticated
public class ProtectedResource {

    @Inject
    JsonWebToken accessToken;

    @GET
    @Path("test-security-jwt")
    public String testSecurityOidc() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
    }

    @GET
    @Path("test-security-jwt-claims")
    public String testSecurityOidcUserInfoMetadata() {
        return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
                + ":" + accessToken.getClaim("email");
    }
}

@TestSecurity アノテーションは常に使用する必要があり、その user プロパティーは JsonWebToken.getName() として返され、roles プロパティーは JsonWebToken.getGroups() として返されることに注意してください。@JwtSecurity アノテーションはオプションであり、追加のトークンクレームを設定するために使用できます。

@TestSecurity@JwtSecurity は、次のようにメタアノテーションで組み合わせることができます:

    @Retention(RetentionPolicy.RUNTIME)
    @Target({ ElementType.METHOD })
    @TestSecurity(user = "userOidc", roles = "viewer")
    @OidcSecurity(introspectionRequired = true,
        introspection = {
            @TokenIntrospection(key = "email", value = "user@gmail.com")
        }
    )
    public @interface TestSecurityMetaAnnotation {

    }

これは、同じセキュリティ設定のセットを複数のテストメソッドで使用する必要がある場合に特に便利です。

ログでエラーを確認する方法

トークンの検証または復号化エラーの詳細を確認するには、io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator TRACE レベルのログを有効にしてください。

quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE

プロアクティブ認証

パブリックエンドポイントメソッド呼び出し時のトークン検証を省略したい場合は、 プロアクティブ認証 を無効にします。

トークンの検証が実行されていない場合、パブリックメソッドで挿入された JsonWebToken にアクセスできないことに注意してください。

SmallRye JWT を直接追加する方法

To parse and verify JsonWebToken with JWTParser, use smallrye-jwt instead of quarkus-smallrye-jwt directly for the following situations:

  • HTTP をサポートしていないQuarkusエクステンション、例えば Quarkus GRPC などを使用している場合。

  • You provide an extension-specific HTTP, the support of which conflicts with the support of those offered by quarkus-smallrye-jwt and Vert.x HTTP, such as Quarkus AWS Lambda.

まず、 smallrye-jwt の依存関係を追加することから始めます:

pom.xml
<dependency>
    <groupId>io.smallrye</groupId>
    <artifactId>smallrye-jwt</artifactId>
</dependency>
build.gradle
implementation("io.smallrye:smallrye-jwt")

さらに、application.properties を更新して、smallrye-jwt によって提供されるすべての CDI プロデューサーを次のように含めます。

quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt

設定リファレンス

Quarkus の設定

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

Configuration property

デフォルト

The MP-JWT configuration object

Environment variable: QUARKUS_SMALLRYE_JWT_ENABLED

Show more

boolean

true

The name of the java.security.Provider that supports SHA256withRSA signatures

Environment variable: QUARKUS_SMALLRYE_JWT_RSA_SIG_PROVIDER

Show more

string

SunRsaSign

Enable this property if fetching the remote keys can be a time-consuming operation. Do not enable it if you use the local keys.

Environment variable: QUARKUS_SMALLRYE_JWT_BLOCKING_AUTHENTICATION

Show more

boolean

false

Always create HTTP 401 challenge, even for requests containing no authentication credentials. JWT authentication mechanism will return HTTP 401 when an authentication challenge is required. However if it is used alongside one of the interactive authentication mechanisms then returning HTTP 401 to the users accessing the application from a browser may not be desired. If you prefer you can request that JWT authentication mechanism does not create a challenge in such cases by setting this property to 'true'.

Environment variable: QUARKUS_SMALLRYE_JWT_SILENT

Show more

boolean

false

MicroProfile JWT の設定

プロパティ名 デフォルト 説明

mp.jwt.verify.publickey

none

The mp.jwt.verify.publickey config property allows the Public Key text itself to be supplied as a string. The Public Key will be parsed from the supplied string in the order defined in the サポートされている公開鍵形式 section.

mp.jwt.verify.publickey.location

none

Config プロパティーを使用すると、公開鍵の外部または内部の場所を指定できます。値は相対パスまたは URL の場合があります。値が HTTPS ベースの JWK セットを指している場合、ネイティブモードで機能するには、quarkus.ssl.native プロパティーも true に設定する必要があります。詳細については ネイティブ実行可能ファイルで SSL を使用する場合 を参照してください。

mp.jwt.verify.publickey.algorithm

RS256

署名アルゴリズム。 ES256 に設定して、楕円曲線署名アルゴリズムをサポートします。

mp.jwt.decrypt.key.location

none

Config プロパティーを使用すると、秘密復号化鍵の外部または内部の場所を指定できます。

mp.jwt.verify.issuer

none

Config プロパティーは、サーバーが有効として受け入れる JWT の iss (発行者) クレームの値を指定します。

mp.jwt.verify.audiences

none

トークン aud クレームに含まれる可能性のあるオーディエンスのコンマ区切りリスト。

mp.jwt.verify.clock.skew

60

トークンの有効期限と年齢確認時に使用されるクロックスキュー(秒)。トークンの有効期限切れ後、現在時刻がこのプロパティで指定された秒数以内であれば、期限切れのトークンは受理されます。デフォルト値は60秒です。

mp.jwt.verify.token.age

none

トークン発行時刻 iat から経過してはならない秒数です。

mp.jwt.token.header

Authorization

Cookie などの別のヘッダーを使用してトークンを渡す場合は、このプロパティーを設定します。

mp.jwt.token.cookie

none

トークンを含む Cookie の名前。このプロパティーは、mp.jwt.token.headerCookie に設定されている場合にのみ有効になります。

追加の SmallRye JWT 設定

SmallRye JWTには、トークン処理をカスタマイズするために使用できる、より多くのプロパティーが用意されています。

プロパティ名 デフォルト 説明

smallrye.jwt.verify.key.location

NONE

公開鍵と秘密鍵の両方を指すことができる検証鍵の場所。秘密鍵は JWK 形式でのみ使用できます。このプロパティーが設定されている場合、'mp.jwt.verify.publickey.location' は無視されることに注意してください。

smallrye.jwt.verify.algorithm

署名アルゴリズム。このプロパティーは、HS256 などの必要な対称アルゴリズムを設定するためにのみ使用する必要があります。ES256 などの非対称アルゴリズムの設定に使用することは推奨されません。代わりに 'mp.jwt.verify.publickey.algorithm' を使用してください。

smallrye.jwt.verify.key-format

ANY

このプロパティーを PEM_KEYPEM_CERTIFICATEJWKJWK_BASE64URL などの特定のキー鍵形式に設定して、検証鍵のロード方法を最適化します。

smallrye.jwt.verify.key-provider

DEFAULT

デフォルトでは、PEM、JWK、または JWK キーセットは、ローカルファイルシステムから読み取るか、MicroProfile JWT 仕様で要求される URI から取得できます。AWS Application Load Balancer の検証キー解決をサポートするには、このプロパティを AWS_ALB に設定します。

smallrye.jwt.verify.relax-key-validation

false

検証鍵の検証を緩和します。このプロパティーを true に設定すると、2048 ビット未満の長さの公開 RSA 鍵が許可されます。

smallrye.jwt.verify.certificate-thumbprint

false

このプロパティーが有効になっている場合、署名されたトークンには 'x5t' または 'x5t#S256' X509Certificatex5t 拇印ヘッダーが含まれている必要があります。この場合、検証鍵は JWK または PEM 証明書鍵形式でのみ使用できます。JWK 鍵には、'x5c' (Base64 でエンコードされた X509Certificate) プロパティーが設定されている必要があります。

smallrye.jwt.token.header

Authorization

Cookie などの別のヘッダーを使用してトークンを渡す場合は、このプロパティーを設定します。このプロパティーは推奨されていません。'mp.jwt.token.header' を使用してください。

smallrye.jwt.key-cache-size

100

キー・キャッシュのサイズ。このプロパティは、 smallrye.jwt.key-cache-time-to-live と同様に、 AWS_ALB のようなキー・プロバイダが、鍵を動的に解決するために smallrye.jwt.verify.key-provider=AWS_ALB で構成されている場合に、キー・キャッシュを制御するために使用します。

smallrye.jwt.key-cache-time-to-live

10

キー・キャッシュ・エントリーの有効期限を分単位で示します。このプロパティは、 smallrye.jwt.key-cache-size と同様に、 AWS_ALB のようなキー・プロバイダが鍵を動的に解決するために smallrye.jwt.verify.key-provider=AWS_ALB で構成されている場合に、キー・キャッシュを制御するために使用します。

smallrye.jwt.token.cookie

none

トークンを含むクッキーの名前。このプロパティは、 smallrye.jwt.token.headerCookie に設定されている場合にのみ有効です。 このプロパティは非推奨です - mp.jwt.token.cookie を使用してください。

smallrye.jwt.always-check-authorization

false

smallrye.jwt.token.headerCookie に設定されていても、名前が smallrye.jwt.token.cookie の Cookie が存在しない場合、Authorization ヘッダーがチェックされるようにこのプロパティーを true に設定します。

smallrye.jwt.token.schemes

Bearer

DPoP などの単一または複数の代替スキームを含むコンマ区切りのリスト。

smallrye.jwt.token.kid

none

鍵の識別子。設定されている場合、検証 JWK 鍵とすべての JWT トークンには、一致する kid ヘッダーが必要です。

smallrye.jwt.time-to-live

none

JWT を使用するために発行できる最大秒数。事実上、JWT の有効期限と発行日との差はこの値を超えてはなりません。このプロパティーを正でない値に設定すると、トークンが有効な 'iat' (で発行された) クレームを持つための要件が緩和されます。

smallrye.jwt.require.named-principal

true

アプリケーションが名前を返す java.security.Principal に依存している場合、トークンには upn または preferred_username または sub クレームが設定されている必要があります。このプロパティーを設定すると、アプリケーションコードが null 以外の Principal 名を確実に処理するためにこれらのクレームのいずれも使用できない場合に、SmallRye JWT は例外を出力します。

smallrye.jwt.path.sub

none

件名を含むクレームへのパス。これはトップレベルの JSON オブジェクトから始まり、複数のセグメントを含めることができます。各セグメントは JSON オブジェクト名のみを表します (例: realms/subject)。このプロパティーは、トークンに sub クレームがないが、サブジェクトが別のクレームに設定されている場合に使用できます。namespace で修飾されたクレームには二重引用符を使用します。

smallrye.jwt.claims.sub

none

現在のトークンに使用可能な標準またはカスタムの sub クレームがない場合に、このプロパティーを使用して、デフォルトのサブクレーム値を設定できます。事実上、このプロパティーを使用して、upnpreferred_usernamesub`クレームが設定されていない場合に `java.security.Principal 名をカスタマイズできます。

smallrye.jwt.path.groups

none

グループを含むクレームへのパス。これはトップレベルの JSON オブジェクトから始まり、複数のセグメントを含めることができます。各セグメントは JSON オブジェクト名のみを表します (例: realm/groups)。このプロパティーは、トークンに 'groups' クレームがないが、グループが別のクレームに設定されている場合に使用できます。namespace で修飾されたクレームには二重引用符を使用します。

smallrye.jwt.groups-separator

space

複数のグループ値を含む可能性のある文字列を分割するためのセパレーター。これは、smallrye.jwt.path.groups プロパティーが値が文字列のカスタムクレームを指している場合にのみ使用されます。標準の OAuth2 scope クレームにはスペースで区切られたシーケンスが含まれている可能性があるため、デフォルト値はスペース 1 個です。

smallrye.jwt.claims.groups

none

このプロパティーを使用して、現在のトークンに使用可能な標準またはカスタムのグループクレームがない場合に、デフォルトのグループクレーム値を設定できます。

smallrye.jwt.jwks.refresh-interval

60

JWK キャッシュの更新間隔 (分単位)。mp.jwt.verify.publickey.location が HTTP または HTTPS URL ベースの JWK セットを指し、正の max-age パラメーター値を持つ HTTP Cache-Control レスポンスヘッダーが JWK HTTPS エンドポイントから返されない限り無視されます。

smallrye.jwt.jwks.forced-refresh-interval

30

強制 JWK キャッシュ更新間隔 (分単位)。現在のトークンの kid ヘッダーと一致する kid プロパティーを持つ JWK キーがキャッシュにないためにトークンの検証が失敗した場合に発生する可能性がある強制更新の試行頻度を制限するために使用されます。mp.jwt.verify.publickey.location が HTTP または HTTPS URL ベースの JWK セットを指していない限り、無視されます。

smallrye.jwt.expiration.grace

0

有効期限を秒単位で指定します。デフォルトでは、現在時刻がトークンの有効期限から1分以内であれば、期限切れのトークンはまだ受け入れられます。このプロパティは非推奨です。代わりに mp.jwt.verify.clock.skew を使用してください。

smallrye.jwt.verify.aud

none

トークン aud クレームに含まれる可能性のあるオーディエンスのコンマ区切りリスト。このプロパティーは推奨されません。mp.jwt.verify.audiences を使用してください。

smallrye.jwt.required.claims

none

トークンに含める必要のあるクレームのコンマ区切りリスト。

smallrye.jwt.decrypt.key.location

none

設定プロパティでは、秘密復号鍵の外部または内部の場所を指定できます。このプロパティは非推奨です。 mp.jwt.decrypt.key.location を使用してください。

smallrye.jwt.decrypt.algorithm

RSA_OAEP

復号化アルゴリズム。

smallrye.jwt.decrypt.key

none

文字列として提供される復号化キー。

smallrye.jwt.token.decryption.kid

none

復号化キー識別子。設定されている場合、復号化 JWK キーとすべての JWT トークンには、一致する kid ヘッダーが必要です。

smallrye.jwt.client.tls.certificate.path

none

キーを HTTPS 経由でフェッチする必要がある場合は、TLS の信頼できる証明書へのパスを設定する必要があります。

smallrye.jwt.client.tls.trust-all

false

すべてのホスト名を信頼します。キーを HTTPS 経由でフェッチする必要があり、このプロパティーが true に設定されている場合、デフォルトですべてのホスト名が信頼されます。

smallrye.jwt.client.tls.hosts

none

信頼できるホスト名のセット。キーを HTTPS 経由でフェッチする必要があり、smallrye.jwt.client.tls.trust-allfalse に設定されている場合、このプロパティーを使用して信頼できるホスト名を設定できます。

smallrye.jwt.http.proxy.host

none

HTTP プロキシーホスト。

smallrye.jwt.http.proxy.port

80

HTTP プロキシーポート。

smallrye.jwt.keystore.type

JKS

mp.jwt.verify.publickey.location または mp.jwt.decrypt.key.locationKeyStore ファイルを指している場合、このプロパティーを使用してキーストアタイプをカスタマイズできます。設定されていない場合は、ファイル名がチェックされ、JKS がデフォルトで設定される前にキーストアタイプが決定されます。

smallrye.jwt.keystore.provider

mp.jwt.verify.publickey.location または mp.jwt.decrypt.key.locationKeyStore ファイルを指している場合、このプロパティーを使用して KeyStore プロバイダーをカスタマイズできます。

smallrye.jwt.keystore.password

キーストアのパスワード。mp.jwt.verify.publickey.location または mp.jwt.decrypt.key.location の場合、このプロパティーが設定されています。

smallrye.jwt.keystore.verify.key.alias

このプロパティーは、mp.jwt.verify.publickey.location`が`KeyStore`ファイルを指している場合に、`KeyStore から抽出される公開検証鍵を一致する証明書から識別するために設定する必要があります。

smallrye.jwt.keystore.decrypt.key.alias

このプロパティは、 mp.jwt.decrypt.key.locationKeyStore ファイルを指している場合、プライベートな復号鍵を特定するために設定する必要があります。

smallrye.jwt.keystore.decrypt.key.password

このプロパティは、 mp.jwt.decrypt.key.locationKeyStore ファイルを指しているときに、 KeyStore にある秘密鍵のパスワードが smallrye.jwt.keystore.password と異なる場合に設定されることがあります。

関連コンテンツ