セキュリティに関するヒントとコツ
Quarkus Security依存関係
io.quarkus:quarkus-security
モジュールには、Quarkus Securityの中核となるクラスが含まれています。
すべてのセキュリティエクステンションですでに提供されているので、ほとんどの場合プロジェクトのビルドファイルに直接追加する必要はありません。しかし、独自のセキュリティコードを書いたり(例えば、 Custom Jakarta REST SecurityContext を登録したり)、 BouncyCastle ライブラリを使用する必要がある場合は、必ずインクルードしてください:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-security</artifactId>
</dependency>
implementation("io.quarkus:quarkus-security")
HttpAuthenticationMechanism のカスタマイズ
CDI実装Beanを登録することで、 HttpAuthenticationMechanism
をカスタマイズすることができます。以下の例では、カスタム認証機能は quarkus-smallrye-jwt
によって提供される JWTAuthMechanism
にデリゲートします。
@Alternative
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism delegate;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
// do some custom action and delegate
return delegate.authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return delegate.getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
return delegate.getCredentialTypes();
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport() {
return delegate.getCredentialTransport();
}
}
The HttpAuthenticationMechanism should transform incoming HTTP request with suitable authentication credentials
into an io.quarkus.security.identity.request.AuthenticationRequest instance and delegate the authentication to the io.quarkus.security.identity.IdentityProviderManager .
Leaving authentication to the io.quarkus.security.identity.IdentityProvider s gives you more options for credentials verifications,
as well as convenient way to perform blocking tasks.
Nevertheless, the io.quarkus.security.identity.IdentityProvider can be omitted and the HttpAuthenticationMechanism is free to authenticate request on its own in trivial use cases.
|
複数のHttpAuthenticationMechanism処理
複数の HttpAuthenticationMechanism
は組み合わせて使っていくことができます。例えば、HTTP Authorization
Basic
または Bearer
スキーム値として渡されたサービスクライアントの資格情報を検証するためには、 quarkus-smallrye-jwt
によって提供される Basic
または JWT
メカニズムを使う必要がありますし、Keycloak やその他の OpenID Connect プロバイダでユーザを認証するには quarkus-oidc
の Authorization Code
メカニズムを使用していく必要があります。
このような場合、 SecurityIdentity
が作成されるまで、メカニズムが順番にクレデンシャルを検証するように要求されます。認証メカニズムは、優先度の降順でソートされています。 Basic
認証メカニズムの優先順位は最高で 2000
、次いで Authorization Code
の優先順位は 1001
、Quarkus が提供する他のすべてのメカニズムの優先順位は 1000
となっています。
例えば、 401
ステータスは Basic
または JWT
のいずれかのメカニズムによって返され、ユーザーを OpenId Connect
プロバイダにリダイレクトする URL は quarkus-oidc
によって返されるなど、認証情報が提供されない場合はメカニズム固有のチャレンジが作成されます。
つまり、 Basic
と Authorization Code
の仕組みが組み合わされた場合、認証情報が提供されなければ 401
が返され、 JWT
と Authorization Code
の仕組みが組み合わされた場合、リダイレクト URL が返されることになります。
このようなデフォルトのチャレンジ選択ロジックが、あるアプリケーションでまさに必要とされる場合もありますが、時には要件を満たさない場合もあります。そのような場合(あるいは、メカニズムが現在の認証またはチャレンジ要求を処理するように求められる順序を変更したい他の同様の場合)、カスタムメカニズムを作成し、どのメカニズムがチャレンジを作成すべきかを選択することができます。 :
@Alternative (1)
@Priority(1)
@ApplicationScoped
public class CustomAwareJWTAuthMechanism implements HttpAuthenticationMechanism {
private static final Logger LOG = LoggerFactory.getLogger(CustomAwareJWTAuthMechanism.class);
@Inject
JWTAuthMechanism jwt;
@Inject
OidcAuthenticationMechanism oidc;
@Override
public Uni<SecurityIdentity> authenticate(RoutingContext context, IdentityProviderManager identityProviderManager) {
return selectBetweenJwtAndOidc(context).authenticate(context, identityProviderManager);
}
@Override
public Uni<ChallengeData> getChallenge(RoutingContext context) {
return selectBetweenJwtAndOidcChallenge(context).getChallenge(context);
}
@Override
public Set<Class<? extends AuthenticationRequest>> getCredentialTypes() {
Set<Class<? extends AuthenticationRequest>> credentialTypes = new HashSet<>();
credentialTypes.addAll(jwt.getCredentialTypes());
credentialTypes.addAll(oidc.getCredentialTypes());
return credentialTypes;
}
@Override
public Uni<HttpCredentialTransport> getCredentialTransport(RoutingContext context) {
return selectBetweenJwtAndOidc(context).getCredentialTransport(context);
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidc(RoutingContext context) {
....
}
private HttpAuthenticationMechanism selectBetweenJwtAndOidcChallenge(RoutingContext context) {
// for example, if no `Authorization` header is available and no `code` parameter is provided - use `jwt` to create a challenge
}
}
1 | メカニズムを代替Beanとして宣言することで、 OidcAuthenticationMechanism や JWTAuthMechanism ではなく、このメカニズムが使用されることが保証されます。 |
セキュリティー・アイデンティティのカスタマイズ
内部的には、アイデンティティ プロバイダは、プリンシパル、役割、クライアント(ユーザー)の認証に使用されたクレデンシャル、 およびその他のセキュリティー属性を保持する io.quarkus.security.identity.SecurityIdentity
クラスのインスタンスを作成して更新します。 SecurityIdentity
をカスタマイズする簡単なオプションは、カスタム SecurityIdentityAugmentor
を登録することです。例えば、以下のオーグメンターは、追加のロールを追加します。 :
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
// Do 'return context.runBlocking(build(identity));'
// if a blocking call is required to customize the identity
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
if(identity.isAnonymous()) {
return () -> identity;
} else {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
// add custom role source here
builder.addRole("dummy");
return builder::build;
}
}
}
以下に、現在の 相互 TLS (mTLS) 認証 リクエストで使用可能なクライアント証明書を使用して、さらにロールを追加する方法を示す別の例を示します:
import java.security.cert.X509Certificate;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;
import java.util.Set;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
return Uni.createFrom().item(build(identity));
}
private Supplier<SecurityIdentity> build(SecurityIdentity identity) {
// create a new builder and copy principal, attributes, credentials and roles from the original identity
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
CertificateCredential certificate = identity.getCredential(CertificateCredential.class);
if (certificate != null) {
builder.addRoles(extractRoles(certificate.getCertificate()));
}
return builder::build;
}
private Set<String> extractRoles(X509Certificate certificate) {
String name = certificate.getSubjectX500Principal().getName();
switch (name) {
case "CN=client":
return Collections.singleton("user");
case "CN=guest-client":
return Collections.singleton("guest");
default:
return Collections.emptySet();
}
}
}
複数のカスタム |
デフォルトでは、セキュリティ・アイデンティティーを拡張する際に、リクエストコンテキストは有効になりません。これは、リクエストコンテキストを必須とするHibernateなどを使用したい場合、 jakarta.enterprise.context.ContextNotActiveException
が発生することを意味します。
解決策は、リクエストコンテキストをアクティブにすることです。次の例では、Hibernate with Panache UserRoleEntity
からロールを取得する方法を示しています。
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Instance;
import jakarta.inject.Inject;
@ApplicationScoped
public class RolesAugmentor implements SecurityIdentityAugmentor {
@Inject
Instance<SecurityIdentitySupplier> identitySupplierInstance;
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
if(identity.isAnonymous()) {
return Uni.createFrom().item(identity);
}
// Hibernate ORM is blocking
SecurityIdentitySupplier identitySupplier = identitySupplierInstance.get();
identitySupplier.setIdentity(identity);
return context.runBlocking(identitySupplier);
}
}
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.runtime.QuarkusSecurityIdentity;
import jakarta.enterprise.context.Dependent;
import jakarta.enterprise.context.control.ActivateRequestContext;
import java.util.function.Supplier;
@Dependent
class SecurityIdentitySupplier implements Supplier<SecurityIdentity> {
private SecurityIdentity identity;
@Override
@ActivateRequestContext
public SecurityIdentity get() {
QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
String user = identity.getPrincipal().getName();
UserRoleEntity.<userRoleEntity>streamAll()
.filter(role -> user.equals(role.user))
.forEach(role -> builder.addRole(role.role));
return builder.build();
}
public void setIdentity(SecurityIdentity identity) {
this.identity = identity;
}
}
プロアクティブ認証が有効な場合、上記の例で示したCDIリクエストコンテキストのアクティブ化は、 RoutingContext
へのアクセスには役立ちません。
次の例は、 SecurityIdentityAugmentor
から RoutingContext
にアクセスする方法を示しています:
package org.acme.security;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.security.identity.AuthenticationRequestContext;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.security.identity.SecurityIdentityAugmentor;
import io.quarkus.vertx.http.runtime.security.HttpSecurityUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {
@Override
public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context,
Map<String, Object> attributes) {
RoutingContext routingContext = HttpSecurityUtils.getRoutingContextAttribute(attributes);
if (routingContext != null) {
// Augment SecurityIdentity using RoutingContext
} else {
return augment(identity, context); (1)
}
}
...
}
1 | HTTPリクエストが完了した後、 SecurityIdentity がオーグメンテーションされると、 RoutingContext は利用できなくなります。 |
カスタム HttpAuthenticationMechanism を実装した場合は、 io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute メソッド呼び出しで認証リクエスト属性に RoutingContext を追加する必要があります。
そうしないと、オーグメンテーション中に RoutingContext を使用できなくなります。
|
カスタムJakarta REST SecurityContext
Jakarta REST ContainerRequestFilter
を使用してカスタムの Jakarta REST SecurityContext
を設定する場合、このカスタムのセキュリティコンテキストを Quarkus SecurityIdentity
とリンクさせるために @PreMatching
アノテーションを追加するなどして、 Jakarta REST の事前一致フェーズで ContainerRequestFilter
が実行されるようにします:
import java.security.Principal;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.container.PreMatching;
import jakarta.ws.rs.core.SecurityContext;
import jakarta.ws.rs.ext.Provider;
@Provider
@PreMatching
public class SecurityOverrideFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String user = requestContext.getHeaders().getFirst("User");
String role = requestContext.getHeaders().getFirst("Role");
if (user != null && role != null) {
requestContext.setSecurityContext(new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return new Principal() {
@Override
public String getName() {
return user;
}
};
}
@Override
public boolean isUserInRole(String r) {
return role.equals(r);
}
@Override
public boolean isSecure() {
return false;
}
@Override
public String getAuthenticationScheme() {
return "basic";
}
});
}
}
}
認可の無効化
認可を無効にする正当な理由がある場合(テスト時など)は、カスタムの AuthorizationController
を登録してください。 :
@Alternative
@Priority(Interceptor.Priority.LIBRARY_AFTER)
@ApplicationScoped
public class DisabledAuthController extends AuthorizationController {
@ConfigProperty(name = "disable.authorization", defaultValue = "false")
boolean disableAuthorization;
@Override
public boolean isAuthorizationEnabled() {
return !disableAuthorization;
}
}
手動テストのために、Quarkusは、開発モードで認証を無効にする便利な設定プロパティを提供します。このプロパティは、上記のカスタム AuthorizationController
とまったく同じ効果を持ちますが、開発モードでのみ利用可能です。 :
quarkus.security.auth.enabled-in-dev-mode=false
また、 TestSecurity
アノテーションを使ってセキュリティチェックを無効にする方法については、 TestingSecurity Annotationの項を参照してください。
セキュリティー プロバイダの登録
デフォルトのプロバイダ
ネイティブモードで実行している場合、GraalVMネイティブ実行可能ファイル生成のデフォルトの動作は、SSLを有効にしていない限り、メインの「SUN」プロバイダのみを含めるようになっています。SSLを有効にしている場合は、すべてのセキュリティープロバイダが登録されます。SSLを使用していない場合は、 quarkus.security.security-providers
プロパティーを使用して、セキュリティープロバイダを名前で選択的に登録することができます。以下の例では、「SunRsaSign」と「SunJCE」のセキュリティープロバイダを登録するための設定を説明します。
quarkus.security.security-providers=SunRsaSign,SunJCE
BouncyCastle
org.bouncycastle.jce.provider.BouncyCastleProvider
JCE プロバイダを登録する必要がある場合は、 BC
プロバイダ名を設定してください :
quarkus.security.security-providers=BC
そして、BouncyCastleプロバイダ依存関係を追加します :
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bcprov-jdk18on")
BouncyCastle JSSE
org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSE プロバイダを登録して、デフォルトの SunJSSE プロバイダの代わりに使用する必要がある場合は、 BCJSSE
というプロバイダ名を設定してください。 :
quarkus.security.security-providers=BCJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
そして、BouncyCastleのTLS依存関係を追加します :
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-jdk18on</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-jdk18on")
BouncyCastle FIPS
org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
JCE プロバイダを登録する必要がある場合は、 BCFIPS
というプロバイダ名を設定してください。 :
quarkus.security.security-providers=BCFIPS
そして、BouncyCastle FIPSプロバイダの依存関係を追加します :
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bc-fips")
|
BouncyCastle JSSE FIPS
デフォルトのSunJSSEプロバイダではなく、 org.bouncycastle.jsse.provider.BouncyCastleJsseProvider
JSSEプロバイダを登録して、 org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider
と組み合わせて使用する必要がある場合は、 BCFIPSJSSE
というプロバイダ名を設定してください。 :
quarkus.security.security-providers=BCFIPSJSSE
quarkus.http.ssl.client-auth=REQUIRED
quarkus.http.ssl.certificate.key-store-file=server-keystore.jks
quarkus.http.ssl.certificate.key-store-password=password
quarkus.http.ssl.certificate.key-store-file-type=BCFKS
quarkus.http.ssl.certificate.key-store-provider=BCFIPS
quarkus.http.ssl.certificate.trust-store-file=server-truststore.jks
quarkus.http.ssl.certificate.trust-store-password=password
quarkus.http.ssl.certificate.trust-store-file-type=BCFKS
quarkus.http.ssl.certificate.trust-store-provider=BCFIPS
そして、BouncyCastle FIPSプロバイダを使用するために最適化されたBouncyCastle TLS依存関係:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bctls-fips</artifactId>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bc-fips</artifactId>
</dependency>
implementation("org.bouncycastle:bctls-fips")
implementation("org.bouncycastle:bc-fips")
keystore と truststore のタイプとプロバイダが BCFKS
と BCFIPS
に設定されていることに注意してください。このタイプとプロバイダでは、以下のようにキーストアを生成することができます。
keytool -genkey -alias server -keyalg RSA -keystore server-keystore.jks -keysize 2048 -keypass password -provider org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider -providerpath $PATH_TO_BC_FIPS_JAR -storetype BCFKS
|
SunPKCS11
SunPKCS11
プロバイダーは、暗号化スマートカードやその他のハードウェアセキュリティモジュール、FIPSモードのネットワークセキュリティサービスなど、特定の PKCS#11
実装へのブリッジを提供します。
通常、 SunPKCS11
を扱うには、 PKCS#11
の実装をインストールし、共有ライブラリやトークンスロットなどを参照する設定を生成し、以下のJavaコードを記述する必要があります。 :
import java.security.Provider;
import java.security.Security;
String configuration = "pkcs11.cfg"
Provider sunPkcs11 = Security.getProvider("SunPKCS11");
Provider pkcsImplementation = sunPkcs11.configure(configuration);
// or prepare configuration in the code or read it from the file such as "pkcs11.cfg" and do
// sunPkcs11.configure("--" + configuration);
Security.addProvider(pkcsImplementation);
Quarkusでは、例えばコードを修正することなく、設定レベルのみで同じことを実現することができます。 :
quarkus.security.security-providers=SunPKCS11
quarkus.security.security-provider-config.SunPKCS11=pkcs11.cfg
|
Reactive Security
リアクティブな環境でセキュリティーを使用する場合は、SmallRye Context Propagationが必要になるでしょう。 :
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-context-propagation</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-context-propagation")
これにより、リアクティブコールバック全体にアイデンティティを伝搬させることができます。また、Quarkusがアイデンティティを伝搬できることを確かにするために、アイデンティティを伝搬できるエグゼキューターを使用していることを確認する必要があります(例: CompletableFuture.supplyAsync
を使用しない場合)。詳細については、 Context Propagation Guideを参照してください。
セキュリティイベントの監視
Quarkus Beanは、 CDIオブザーバー を使用して、認証や認可のセキュリティイベントを消費することができます。 オブザーバは同期でも非同期でもかまいません。
-
io.quarkus.security.spi.runtime.AuthenticationFailureEvent
-
io.quarkus.security.spi.runtime.AuthenticationSuccessEvent
-
io.quarkus.security.spi.runtime.AuthorizationFailureEvent
-
io.quarkus.security.spi.runtime.AuthorizationSuccessEvent
-
io.quarkus.oidc.SecurityEvent
-
io.quarkus.vertx.http.runtime.security.FormAuthenticationEvent
Quarkus OpenID Connectエクステンション固有のセキュリティイベントの詳細については、Webアプリケーションを保護するためのOIDCコードフローメカニズムガイドの 重要な認証イベントのリスニング セクションを参照してください。 |
package org.acme.security;
import io.quarkus.security.spi.runtime.AuthenticationFailureEvent;
import io.quarkus.security.spi.runtime.AuthenticationSuccessEvent;
import io.quarkus.security.spi.runtime.AuthorizationFailureEvent;
import io.quarkus.security.spi.runtime.AuthorizationSuccessEvent;
import io.quarkus.security.spi.runtime.SecurityEvent;
import io.vertx.ext.web.RoutingContext;
import jakarta.enterprise.event.Observes;
import jakarta.enterprise.event.ObservesAsync;
import org.jboss.logging.Logger;
public class SecurityEventObserver {
private static final Logger LOG = Logger.getLogger(SecurityEventObserver.class.getName());
void observeAuthenticationSuccess(@ObservesAsync AuthenticationSuccessEvent event) { (1)
LOG.debugf("User '%s' has authenticated successfully", event.getSecurityIdentity().getPrincipal().getName());
}
void observeAuthenticationFailure(@ObservesAsync AuthenticationFailureEvent event) {
RoutingContext routingContext = (RoutingContext) event.getEventProperties().get(RoutingContext.class.getName());
LOG.debugf("Authentication failed, request path: '%s'", routingContext.request().path());
}
void observeAuthorizationSuccess(@ObservesAsync AuthorizationSuccessEvent event) {
String principalName = getPrincipalName(event);
if (principalName != null) {
LOG.debugf("User '%s' has been authorized successfully", principalName);
}
}
void observeAuthorizationFailure(@Observes AuthorizationFailureEvent event) {
LOG.debugf(event.getAuthorizationFailure(), "User '%s' authorization failed", event.getSecurityIdentity().getPrincipal().getName());
}
private static String getPrincipalName(SecurityEvent event) { (2)
if (event.getSecurityIdentity() != null) {
return event.getSecurityIdentity().getPrincipal().getName();
}
return null;
}
}
1 | このオブザーバーはすべての AuthenticationSuccessEvent イベントを非同期に消費します。つまり、HTTP リクエスト処理はイベント処理に関係なく継続されます。
アプリケーションによっては、 AuthenticationSuccessEvent イベントは大量になります。
そのため、非同期処理はパフォーマンスに良い影響を与えます。 |
2 | サポートされているすべてのセキュリティ・イベント・タイプは、すべて io.quarkus.security.spi.runtime.SecurityEvent インターフェースを実装しているので、共通のコードで扱うことが出来ます。 |