プロアクティブ認証
設定のカスタマイズや例外処理など、Quarkusでプロアクティブ認証を管理する方法を学びます。 さまざまなアプリケーションシナリオに対する実践的な洞察と戦略を得ることができます。
Quarkusでは、プロアクティブ認証がデフォルトで有効になっています。 これにより、ターゲットページが認証を必要としない場合でも、認証情報を持つすべての受信リクエストが認証されます。 その結果、ターゲットページが公開されている場合でも、無効な認証情報を持つリクエストは拒否されます。 匿名リクエストが許可されているため、認証情報のないリクエストは拒否されません。
ターゲット・ページが要求したときにのみ認証を行いたい場合は、このデフォルトの動作をオフにすることができます。
プロアクティブ認証をオフにして、ターゲット・ページが要求したときにのみ認証を行なうようにするには、 application.properties
の設定ファイルを以下のように変更します:
quarkus.http.auth.proactive=false
プロアクティブ認証をオフにすると、ID が要求されたときにのみ認証プロセスが実行されます。 ID が要求されるのは、ユーザ認証を必要とするセキュリティ・ルールがあるため、または現在の ID へのプログラムによるアクセスが必要なためです。
プロアクティブ認証が使用されている場合、 SecurityIdentity
へのアクセスはブロッキング操作となります。
これは、認証がまだ行われていない可能性があり、 SecurityIdentity
へのアクセスにはデータベースなどの外部システムへの呼び出しが必要になるため、操作がブロックされる可能性があるためです。
ブロッキング・アプリケーションの場合、これは問題ではありません。
しかし、リアクティブなアプリケーションで認証を無効にしている場合は、 I/O スレッドでブロッキング操作を行うことができないため、失敗します。
これを回避するには、 io.quarkus.security.identity.CurrentIdentityAssociation
のインスタンスを @Inject
し、 Uni<SecurityIdentity> getDeferredIdentity();
メソッドを呼び出します。
そして、認証が完了して ID が利用可能になったときに通知されるように、生成された Uni
をサブスクライブします。
認証がすでに行われているため、 |
プロアクティブ認証が無効になっている場合、void ではないセキュリティー保護されたメソッドが同期的に値を返すと、CDI Bean で使用される 標準セキュリティーアノテーション は I/O スレッドで機能しません。
この制限は、これらのメソッドが SecurityIdentity
にアクセスする必要があることが原因です。
次の例では、HelloResource
と HelloService
を定義します。
/hello
へのすべての GET リクエストは I/O スレッドで実行され、BlockingOperationNotAllowedException
例外が出力されます。
例の修正方法は1つではありません:
-
hello
エンドポイントに@Blocking
アノテーションを付けて、ワーカースレッドに切り替えます。 -
リアクティブまたは非同期データ型を使用して、
sayHello
メソッドの戻り値の型を変更します。 -
@RolesAllowed
アノテーションをエンドポイントに移動します。 エンドポイントメソッドからのSecurityIdentity
へのアクセスは決してブロッキング操作ではないので、これは最も安全な方法のひとつです。
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.smallrye.mutiny.Uni;
@Path("/hello")
@PermitAll
public class HelloResource {
@Inject
HelloService helloService;
@GET
public Uni<String> hello() {
return Uni.createFrom().item(helloService.sayHello());
}
}
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.ApplicationScoped;
@ApplicationScoped
public class HelloService {
@RolesAllowed("admin")
public String sayHello() {
return "Hello";
}
}
CDI リクエストコンテキストの有効化
認証および認可の際に、@RequestScoped
の Bean をインジェクトする必要がある場合があります。
適切な例として、SecurityIdentity
の拡張時にデータベースへアクセスするケースが挙げられます。これについては、"Security Tips and Tricks" ガイドの セキュリティーアイデンティティーカスタマイズ セクションで説明されています。
認証や認可の際に jakarta.enterprise.context.ContextNotActiveException
が発生する場合、プロアクティブ認証を無効化する のが最も適切な解決策となることが多いです。
また、ユーザーは CDI リクエストコンテキスト を有効化することもできます。たとえば、@ActivateRequestContext
アノテーションを使用して有効にできます。ただし、一部の CDI Bean は利用可能な状態になっていない可能性がある点に注意してください。
このソリューションの例外は、アプリケーションエンドポイントが 設定を使用した承認 で保護されている場合です。 詳細は、「Web エンドポイントの認可」ガイドの RequestScoped Bean を HttpSecurityPolicy に挿入する セクションを参照してください。
認証例外応答のカスタマイズ
Jakarta REST ExceptionMapper
を使用して、 io.quarkus.security.AuthenticationFailedException
のような Quarkus Security 認証の例外をキャプチャできます。
例:
package io.quarkus.it.keycloak;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.core.UriInfo;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.security.AuthenticationFailedException;
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFailedExceptionMapper implements ExceptionMapper<AuthenticationFailedException> {
@Context
UriInfo uriInfo;
@Override
public Response toResponse(AuthenticationFailedException exception) {
return Response.status(401).header("WWW-Authenticate", "Basic realm=\"Quarkus\"").build();
}
}
一部の HTTP 認証メカニズムでは、正しい認証チャレンジを作成するために、認証例外を自ら処理する必要があります。
たとえば、OpenID Connect (OIDC) 認可コードフロー認証を管理する io.quarkus.oidc.runtime.CodeAuthenticationMechanism は、正しいリダイレクト URL を構築し、状態 Cookie を設定する必要があります。
したがって、このようなメカニズムによって出力される認証例外をカスタマイズするためにカスタム例外マッパーを使用することは避けてください。
代わりに、より安全なアプローチとしては、プロアクティブ認証が有効になっていることを確認し、Vert.x HTTP ルート失敗ハンドラーを使用してください。
これは、イベントが正しい応答ステータスとヘッダーとともにハンドラーに送信されるためです。
次に、応答のみをカスタマイズする必要があります。例:
|
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import io.quarkus.security.AuthenticationFailedException;
import io.vertx.core.Handler;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
@ApplicationScoped
public class AuthenticationFailedExceptionHandler {
public void init(@Observes Router router) {
router.route().failureHandler(new Handler<RoutingContext>() {
@Override
public void handle(RoutingContext event) {
if (event.failure() instanceof AuthenticationFailedException) {
event.response().end("CUSTOMIZED_RESPONSE");
} else {
event.next();
}
}
});
}
}