Secure a Quarkus application with Basic authentication

Secure your Quarkus application endpoints by combining Quarkus built-in basic HTTP authentication with the JPA identity provider to enable role-based access control (RBAC). The JPA IdentityProvider creates a SecurityIdentity instance, which is used during user authentication to verify and authorize access requests making your Quarkus application secure.

このチュートリアルでは、Quarkusでより高度なセキュリティメカニズムを実装するための準備、たとえば、OpenID Connect(OIDC)認証メカニズムの使用方法について説明します。

前提条件

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

  • 約15分

  • IDE

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

  • Apache Maven 3.8.6

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

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

何を構築するか

このチュートリアルでは、以下のエンドポイントを提供するアプリケーションを構築する方法を説明します:

エンドポイント 説明

/api/public

/api/public エンドポイントには匿名でアクセスすることができます。

/api/admin

/api/admin のエンドポイントは RBAC で保護されており、 admin のロールを付与されたユーザーのみがアクセスすることができます。 このエンドポイントでは、アクセス制約を宣言的に課すために @RolesAllowed アノテーションが使用されています。

/api/users/me

/api/users/me のエンドポイントは RBAC で保護されており、 user のロールを付与されたユーザーのみがアクセスすることができます。ユーザーに関する詳細情報を含む JSON ドキュメントがレスポンスとして返されます。

コードを確かめるだけなら、次のいずれかの方法で、完成例まで早送りすることができます:

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

1. Maven プロジェクトの作成

Quarkus セキュリティでセキュリティソースを JPA エンティティにマッピングするには、このチュートリアルで使用する Maven プロジェクト に security-jpa エクステンションが含まれていることを確認します。 security-jpa エクステンションを持つ新しい Maven プロジェクトを作成するか、既存の Maven プロジェクト にエクステンションを追加します。

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

コマンドラインインタフェース
quarkus create app org.acme:security-jpa-quickstart \
    --stream=2.16 \
    --extension='security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache' \
    --no-code
cd security-jpa-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-jpa-quickstart \
    -Dextensions='security-jpa,jdbc-postgresql,resteasy-reactive,hibernate-orm-panache' \
    -DnoCode
cd security-jpa-quickstart

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

ユーザー ID の保存には Hibernate ORM with Panache が使用されていますが、Hibernate ORM を使用することも可能です。また、お好みのデータベースコネクタライブラリを追加する必要があります。 このチュートリアルの例では、ID ストアに PostgreSQL データベースを使用しています。

  • 既存の Maven プロジェクトに security-jpa エクステンションを追加するには、プロジェクトのベースディレクトリで次のコマンドを実行します:

CLI
quarkus extension add 'security-jpa'
Maven
./mvnw quarkus:add-extension -Dextensions='security-jpa'
Gradle
./gradlew addExtension --extensions='security-jpa'

1.1. 検証

どちらかのコマンドを実行すると、ビルドファイルに以下のようなXMLが追加されます。:

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

2. アプリケーションの記述

  • まず、すべてのユーザーがアプリケーションにアクセスできるように、 /api/public エンドポイントを実装しましょう。 次のコードスニペットのように、通常のJAX-RSリソースをJavaソースコードに追加してください:

    package org.acme.security.jpa;
    
    import javax.annotation.security.PermitAll;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.core.MediaType;
    
    @Path("/api/public")
    public class PublicResource {
    
        @GET
        @PermitAll
        @Produces(MediaType.TEXT_PLAIN)
        public String publicResource() {
            return "public";
       }
    }
  • api/admin エンドポイントのソースコードも同様ですが、代わりに @RolesAllowed アノテーションを使用して、 admin ロールを与えられたユーザのみがエンドポイントにアクセスできることを確認します。 JAX-RS リソースを追加して、以下の @RolesAllowed アノテーションを付けます:

    package org.acme.security.jpa;
    
    import javax.annotation.security.RolesAllowed;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.Produces;
    import javax.ws.rs.core.MediaType;
    
    @Path("/api/admin")
    public class AdminResource {
    
        @GET
        @RolesAllowed("admin")
        @Produces(MediaType.TEXT_PLAIN)
        public String adminResource() {
             return "admin";
        }
    }
  • 最後に、 /api/users/me エンドポイントを考えてみましょう。下のソースコードを見ればわかるように、 user の役割を持つユーザのみを信頼しています。現在認証されている Principal へのアクセスを得るために SecurityContext を使用しており、ユーザーの名前を返します。この情報はデータベースから読み込まれます。

    package org.acme.security.jpa;
    
    import javax.annotation.security.RolesAllowed;
    import javax.inject.Inject;
    import javax.ws.rs.GET;
    import javax.ws.rs.Path;
    import javax.ws.rs.core.Context;
    import javax.ws.rs.core.SecurityContext;
    
    @Path("/api/users")
    public class UserResource {
    
        @GET
        @RolesAllowed("user")
        @Path("/me")
        public String me(@Context SecurityContext securityContext) {
            return securityContext.getUserPrincipal().getName();
        }
    }

2.1. ユーザーエンティティーの定義

次のコードで説明するように、 user エンティティにアノテーションを追加することで、セキュリティ情報をどのようにモデルに格納したいかを記述することができます:

package org.acme.security.jpa;

import javax.persistence.Entity;
import javax.persistence.Table;

import io.quarkus.hibernate.orm.panache.PanacheEntity;
import io.quarkus.elytron.security.common.BcryptUtil;
import io.quarkus.security.jpa.Password;
import io.quarkus.security.jpa.Roles;
import io.quarkus.security.jpa.UserDefinition;
import io.quarkus.security.jpa.Username;

@Entity
@Table(name = "test_user")
@UserDefinition (1)
public class User extends PanacheEntity {
    @Username (2)
    public String username;
    @Password (3)
    public String password;
    @Roles (4)
    public String role;

    /**
     * Adds a new user to the database
     * @param username the username
     * @param password the unencrypted password (it will be encrypted with bcrypt)
     * @param role the comma-separated roles
     */
    public static void add(String username, String password, String role) { (5)
        User user = new User();
        user.username = username;
        user.password = BcryptUtil.bcryptHash(password);
        user.role = role;
        user.persist();
    }
}

security-jpa エクステンションは、 @UserDefinition でアノテーションされた単一のエンティティーがある場合にのみ初期化されます。

1 アノテーション @UserDefinition は単一のエンティティに存在する必要があり、通常の Hibernate ORM エンティティまたは Panache 付き Hibernate ORM エンティティのいずれかにすることができます。
2 ユーザー名に使用されるフィールドを示します。
3 パスワードに使用するフィールドを示します。デフォルトではbcryptハッシュ化されたパスワードを使用しますが、プレーンテキストまたはカスタムパスワード用に設定することも可能です。
4 ここでは対象のプリンシパル表現属性に追加されたロールのコンマ区切りリストを示します。
5 この方法では、パスワードを適切なbcryptハッシュでハッシュしながらユーザーを追加することができます。

2.2. アプリケーションの設定

  1. Enable Quarkus built-in basic HTTP authentication by setting the quarkus.http.auth.basic property to true:

    quarkus.http.auth.basic=true

    When secure access is required and no other authentication mechanisms are enabled, Quarkus built-in basic HTTP authentication is the fallback authentication mechanism. Therefore, in this tutorial, you do not need to set the property quarkus.http.auth.basic=true.

  2. 少なくとも1つのデータソースを設定し、 security-jpa エクステンションがデータベースにアクセスできるようにします。

    quarkus.http.auth.basic=true
    
    quarkus.datasource.db-kind=postgresql
    quarkus.datasource.username=quarkus
    quarkus.datasource.password=quarkus
    quarkus.datasource.jdbc.url=jdbc:postgresql:security_jpa
    
    quarkus.hibernate-orm.database.generation=drop-and-create
  3. データベースをユーザーとロールで初期化するには、次のコードで説明するように Startup クラスを実装します:

このチュートリアルでは、PostgreSQL データベースを ID ストアに使用します。Hibernate ORM は起動時にデータベーススキーマを自動的に作成します (運用時にはこれを変更します)。

package org.acme.security.jpa;

import javax.enterprise.event.Observes;
import javax.inject.Singleton;
import javax.transaction.Transactional;

import io.quarkus.runtime.StartupEvent;


@Singleton
public class Startup {
    @Transactional
    public void loadUsers(@Observes StartupEvent evt) {
        // reset and load all test users
        User.deleteAll();
        User.add("admin", "admin", "admin");
        User.add("user", "user", "user");
    }
}

これでアプリケーションは保護され、ユーザーIDは指定されたデータベースから提供されるようになりました。

本番環境では、プレーンテキストのパスワードは保存しないでください。 そのため、 security-jpa はデフォルトで bcrypt でハッシュ化したパスワードを使用します。

3. アプリケーションのテスト

3.1. Dev Services for PostgreSQLを使用してアプリケーションをテストする

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

アプリケーションを実稼働モードで動作させる前に、統合テストを追加してみましょう。

JVMとネイティブモードでのアプリケーションの統合テストには、 Dev Services for PostgreSQL を使用します。

以下のプロパティ構成は、PostgreSQLのテストを本番環境( prod )モードのみで実行できるようにする方法を示しています。このシナリオでは、 Dev Services for PostgreSQL が PostgreSQL テストコンテナを起動し、構成します。

%prod.quarkus.datasource.db-kind=postgresql
%prod.quarkus.datasource.username=quarkus
%prod.quarkus.datasource.password=quarkus
%prod.quarkus.datasource.jdbc.url=jdbc:postgresql:elytron_security_jpa

quarkus.hibernate-orm.database.generation=drop-and-create

プロファイルの接頭辞に %prod. を付けると、データソースのプロパティは Dev Services for PostgreSQL からは見えなくなり、本番モードで動作するアプリケーションからのみ表示されるようになります。

統合テストを書くには、次のコードサンプルを使用します:

package org.acme.elytron.security.jpa;

import static io.restassured.RestAssured.get;
import static io.restassured.RestAssured.given;
import static org.hamcrest.core.Is.is;

import org.apache.http.HttpStatus;
import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

@QuarkusTest
public class JpaSecurityRealmTest {

    @Test
    void shouldAccessPublicWhenAnonymous() {
        get("/api/public")
                .then()
                .statusCode(HttpStatus.SC_OK);

    }

    @Test
    void shouldNotAccessAdminWhenAnonymous() {
        get("/api/admin")
                .then()
                .statusCode(HttpStatus.SC_UNAUTHORIZED);

    }

    @Test
    void shouldAccessAdminWhenAdminAuthenticated() {
        given()
                .auth().preemptive().basic("admin", "admin")
                .when()
                .get("/api/admin")
                .then()
                .statusCode(HttpStatus.SC_OK);

    }

    @Test
    void shouldNotAccessUserWhenAdminAuthenticated() {
        given()
                .auth().preemptive().basic("admin", "admin")
                .when()
                .get("/api/users/me")
                .then()
                .statusCode(HttpStatus.SC_FORBIDDEN);
    }

    @Test
    void shouldAccessUserAndGetIdentityWhenUserAuthenticated() {
        given()
                .auth().preemptive().basic("user", "user")
                .when()
                .get("/api/users/me")
                .then()
                .statusCode(HttpStatus.SC_OK)
                .body(is("user"));
    }
}

このコードサンプルからわかるように、テストコードからテストコンテナーを起動する必要はありません。

アプリケーションを開発モードで起動すると、 Dev Services for PostgreSQL が PostgreSQL devmode コンテナを起動し、アプリケーションの開発を開始することができるようになります。 アプリケーションの開発中に、継続的テスト の機能を使って、テストを一つずつ追加して実行することも可能です。 Dev Services for PostgreSQL は、 devmode コンテナと衝突しない独立した PostgreSQL テストコンテナを提供することで、開発中のテストをサポートします。

3.2. Use curl or a browser to test your application

下記の例のように、PostgreSQL サーバーを起動します:

docker run --rm=true --name security-getting-started -e POSTGRES_USER=quarkus \
           -e POSTGRES_PASSWORD=quarkus -e POSTGRES_DB=elytron_security_jpa \
           -p 5432:5432 postgres:14.1

3.3. アプリケーションのコンパイルと実行

次のいずれかの方法で、Quarkusアプリケーションをコンパイルして実行します:

3.3.1. JVMモード

アプリケーションをコンパイルします。

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

アプリケーションを実行します。

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

3.3.2. ネイティブモード

アプリケーションをコンパイルします。

CLI
quarkus build --native
Maven
./mvnw install -Dnative
Gradle
./gradlew build -Dquarkus.package.type=native

アプリケーションを実行します。

./target/security-jpa-quickstart-runner

3.4. アプリケーションのセキュリティへのアクセスおよびテスト

アプリケーションの実行中は、以下の curl コマンドのいずれかを使用して、アプリケーションにアクセスできます。また、ブラウザを使用して同じエンドポイントURLにアクセスすることもできます。

  • 保護されたリソースを匿名で攻撃してみましょう:

$ curl -i -X GET http://localhost:8080/api/public
HTTP/1.1 200 OK
Content-Length: 6
Content-Type: text/plain;charset=UTF-8

public%
  • 保護されたリソースを匿名で攻撃してみましょう:

$ curl -i -X GET http://localhost:8080/api/admin
HTTP/1.1 401 Unauthorized
Content-Length: 14
Content-Type: text/html;charset=UTF-8
WWW-Authenticate: Basic

Not authorized%

ブラウザーを使って保護されたリソースに匿名で接続すると、認証情報を入力するよう促すBasic認証フォームが表示されます。

  • 認証されたユーザーとして保護されたエンドポイントに接続します:

$ curl -i -X GET -u admin:admin http://localhost:8080/api/admin
HTTP/1.1 200 OK
Content-Length: 5
Content-Type: text/plain;charset=UTF-8

admin%

3.5. 結果

許可されたユーザーの資格情報、例えば admin:admin を提供すると、JPA セキュリティエクステンションはそのユーザーのロールを認証してロードします。 admin ユーザーは保護されたリソースへのアクセスを許可されます。

リソースが @RolesAllowed("user") で保護されている場合、次のシェルの例で説明するように、ユーザー admin は "user" ロールに割り当てられていないため、リソースへのアクセスは許可されません:

$ curl -i -X GET -u admin:admin http://localhost:8080/api/users/me
HTTP/1.1 403 Forbidden
Content-Length: 34
Content-Type: text/html;charset=UTF-8

Forbidden%

最後に、ユーザ名 user が認証され、セキュリティコンテキストには、例えばユーザ名などの主要な詳細が含まれます。

$ curl -i -X GET -u user:user http://localhost:8080/api/users/me
HTTP/1.1 200 OK
Content-Length: 4
Content-Type: text/plain;charset=UTF-8

user%

Quarkus Security JPA 情報

セキュリティクイックスタートプロジェクトの実行とテストに成功したので、Quarkus SecurityとJPA IDストアのセキュリティ機能を理解していくための準備ができました。

サポートされているモデルの種類

  • @UserDefinition クラスは JPA エンティティーである必要があります(Panache を使用しているかどうかは問いません)。

  • @Username と @Password フィールドの型は String でなければなりません。

  • @Roles フィールドは String か Collection<String> のいずれかのタイプであるか、または Collection<X> である必要があります。 X はエンティティークラスで、 @RolesValue アノテーションが付与された String フィールドが 1 つあります。

  • 各 String role 要素の型は、カンマで区切られたロールのリストとして解析されます。

別のエンティティーにロールを格納する

次のサンプルを使用して、別のエンティティ内にロールを格納します:

@UserDefinition
@Table(name = "test_user")
@Entity
public class User extends PanacheEntity {
    @Username
    public String name;

    @Password
    public String pass;

    @ManyToMany
    @Roles
    public List<Role> roles = new ArrayList<>();
}

@Entity
public class Role extends PanacheEntity {

    @ManyToMany(mappedBy = "roles")
    public List<ExternalRolesUserEntity> users;

    @RolesValue
    public String role;
}

パスワードの保存とハッシュ化

デフォルトでは、Modular Crypt Format (MCF) のもと、bcrypt を用いてパスワードの保存とハッシュ化が行われます。

このようなハッシュ化されたパスワードを作成する必要がある場合は、便利な String BcryptUtil.bcryptHash(String password) 関数を用意しています。デフォルトでは、ランダムなソルトを作成して 10 回の繰り返しでハッシュ化します (繰り返しとソルトを指定することもできます)。

When you use MCF, you don’t need dedicated columns to store the hashing algorithm, the iterations count, or the salt because they are all stored in the hashed value.

また、例えば @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class) といった形で別のハッシュアルゴリズムを使ってパスワードを保存することもできます:

@UserDefinition
@Table(name = "test_user")
@Entity
public class CustomPasswordUserEntity {
    @Id
    @GeneratedValue
    public Long id;

    @Column(name = "username")
    @Username
    public String name;

    @Column(name = "password")
    @Password(value = PasswordType.CUSTOM, provider = CustomPasswordProvider.class)
    public String pass;

    @Roles
    public String role;
}

public class CustomPasswordProvider implements PasswordProvider {
    @Override
    public Password getPassword(String pass) {
        byte[] digest = DatatypeConverter.parseHexBinary(pass);
        return SimpleDigestPassword.createRaw(SimpleDigestPassword.ALGORITHM_SIMPLE_DIGEST_SHA_256, digest);
    }
}
In a test environment, you can also store passwords in plain text by using @Password(PasswordType.CLEAR). For applications running in production, do not store passwords in plain text.

What’s next

Congratulations! You have learned how to create and test a secure Quarkus application by combining the Quarkus built-in basic HTTP authentication with the JPA identity provider.

このチュートリアルを完了したら、Quarkus のより高度なセキュリティメカニズムをいくつか検討してください。 次の情報を使用して、 OpenID Connect を安全に使用して、Quarkusのエンドポイントに安全なシングルサインオンアクセスを提供する方法について学びます: