セキュリティに関するヒントとコツ
Quarkus Security依存関係
io.quarkus:quarkus-security
モジュールには、Quarkus Securityの中核となるクラスが含まれています。
In most cases, it does not have to be added directly to your project’s build file as it is already provided by all the security extensions. However, if you need to write your own custom security code (for example, register a Custom Jakarta REST SecurityContext) or use BouncyCastle libraries, then please make sure it is included:
<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;
}
}
}
Here is another example showing how to use the client certificate available in the current mutual TLS (mTLS) authentication request to add more roles:
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;
}
}
The CDI request context activation shown in the example above does not help you to access the RoutingContext
when the proactive authentication is enabled.
The following example illustrates how you can access the RoutingContext
from the SecurityIdentityAugmentor
:
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 | The RoutingContext is not be available when the SecurityIdentity is augmented after HTTP request has completed. |
If you implemented a custom HttpAuthenticationMechanism , then you need to add the RoutingContext to the authentication
request attributes with the io.quarkus.vertx.http.runtime.security.HttpSecurityUtils.setRoutingContextAttribute method call.
Otherwise, the RoutingContext will not be available during augmentation.
|
カスタム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を参照してください。
Observe security events
Quarkus beans can use CDI observers to consume authentication and authorization security events. The observers can be either synchronous or asynchronous.
-
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
For more information about security events specific to the Quarkus OpenID Connect extension, please see the Listening to important authentication events section of the OIDC code flow mechanism for protecting web applications guide.
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 | This observer consumes all the AuthenticationSuccessEvent events asynchronously, which means that HTTP request processing will continue regardless on the event processing.
Depending on the application, that can be a lot of the AuthenticationSuccessEvent events.
For that reason, asynchronous processing can have positive effect on performance. |
2 | Common code for all supported security event types is possible because they all implement the io.quarkus.security.spi.runtime.SecurityEvent interface. |