The English version of quarkus.io is the official project site. Translated sites are community supported on a best-effort basis.
このページを編集

Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム

Quarkus OIDC エクステンションが提供する業界標準の OpenID Connect (OIDC) 認可コードフローメカニズムを使用して、Web アプリケーションを保護することができます。

OIDC 認可コードフローメカニズムの概要

Quarkus OpenID Connect (OIDC) エクステンションは、Keycloak などの OIDC 準拠の認可サーバーでサポートされている OIDC 認可コードフローメカニズムを使用して、アプリケーション HTTP エンドポイントを保護できます。

認可コードフローメカニズムは、Web アプリケーションのユーザーを Keycloak などの OIDC プロバイダーにリダイレクトしてログインさせることで、ユーザーを認証します。 認証後、OIDC プロバイダーは、認証が成功したことを確認する認可コードを使用してユーザーをアプリケーションにリダイレクトします。 次に、アプリケーションは、このコードを OIDC プロバイダーと交換して、ID トークン (認証されたユーザーを表す)、アクセストークン、およびリフレッシュトークンを取得し、ユーザーのアプリケーションへのアクセスを認可します。

次の図は、Quarkus における認可コードフローメカニズムの概要を示しています。

Authorization Code Flow
Figure 1. Quarkus における認可コードフローメカニズム
  1. Quarkus ユーザーは、Quarkus web-app アプリケーションへのアクセスをリクエストします。

  2. Quarkus web-app は、ユーザーを認可エンドポイント、つまり認証用の OIDC プロバイダーにリダイレクトします。

  3. OIDC プロバイダーは、ユーザーをログインと認証のプロンプトにリダイレクトします。

  4. プロンプトで、ユーザーは自分のユーザークレデンシャルを入力します。

  5. OIDC プロバイダーは、入力されたユーザークレデンシャルを認証し、認証に成功すると認可コードを発行し、そのコードをクエリーパラメーターとして含めて Quarkus web-app にユーザーをリダイレクトします。

  6. Quarkus web-app は、この認可コードを OIDC プロバイダーと交換し、ID、アクセス、およびリフレッシュの各トークンを取得します。

認可コードフローが完了し、Quarkus web-app は発行されたトークンを使用してユーザーに関する情報にアクセスし、そのユーザーに関連するロールベースの認可を付与します。 発行されるトークンは以下のとおりです。

  • IDトークン: Quarkus web-app アプリケーションは、ID トークンのユーザー情報を使用して、認証されたユーザーが安全にログインできるようにし、Web アプリケーションにロールベースのアクセスを提供します。

  • アクセストークン: Quarkus web-app は、アクセストークンを使用して UserInfo API にアクセスし、認証されたユーザーに関する追加情報を取得したり、別のエンドポイントに伝播したりすることがあります。

  • リフレッシュトークン: (オプション) ID およびアクセストークンの有効期限が切れた場合、Quarkus web-app はリフレッシュトークンを使用して新しい ID およびアクセストークンを取得できます。

OIDC設定プロパティ のリファレンスガイドもご参照ください。

OIDC 認可コードフローメカニズムを使用して Web アプリケーションを保護する方法については、OIDC 認可コードフローを使用した Web アプリケーションの保護 を参照してください。

OIDC ベアラートークン認証を使用してサービスアプリケーションを保護したい場合は、OIDC ベアラートークン認証 を参照してください。

マルチテナントへの対応方法については、OpenID Connect (OIDC) マルチテナンシーの使用 ガイドをお読みください。

認可コードフローメカニズムの利用について

OIDC プロバイダーエンドポイントへのアクセス設定

OIDC web-app アプリケーションは、OIDC プロバイダーの認可、トークン、JsonWebKey (JWK) セット、そして場合によっては UserInfo、イントロスペクション、セッション終了 (RP-initiated logout) エンドポイントの URL を必要とします。

規約により、quarkus.oidc.auth-server-url で設定することで /.well-known/openid-configuration のパスを追加することで検出されます。

また、ディスカバリーエンドポイントが利用できない場合や、ディスカバリーエンドポイントのラウンドトリップを減らしたい場合は、エンドポイントのディスカバリーを無効にし、相対パス値を設定することもできます。 例:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Authorization endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/auth
quarkus.oidc.authorization-path=/protocol/openid-connect/auth
# Token endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token
quarkus.oidc.token-path=/protocol/openid-connect/token
# JWK set endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/certs
quarkus.oidc.jwks-path=/protocol/openid-connect/certs
# UserInfo endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/userinfo
quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo
# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/token/introspect
# End-session endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/logout
quarkus.oidc.end-session-path=/protocol/openid-connect/logout

一部の OIDC プロバイダーは、メタデータディスカバリーをサポートしていますが、認可コードフローを完了したり、ユーザーログアウトなどのアプリケーション機能をサポートしたりするために必要なすべてのエンドポイント URL 値は返しません。 この制限を回避するには、次の例に示すように、不足しているエンドポイント URL 値をローカルで設定します。

# Metadata is auto-discovered but it does not return an end-session endpoint URL

quarkus.oidc.auth-server-url=http://localhost:8180/oidcprovider/account

# Configure the end-session URL locally.
# It can be an absolute or relative (to 'quarkus.oidc.auth-server-url') address
quarkus.oidc.end-session-path=logout

検出されたエンドポイント URL がローカルの Quarkus エンドポイントで機能せず、より具体的な値が必要な場合は、この同じ設定を使用して、エンドポイント URL をオーバーライドできます。 たとえば、グローバルエンドセッションエンドポイントとアプリケーション固有のエンドセッションエンドポイントの両方をサポートするプロバイダーは、http://localhost:8180/oidcprovider/account/global-logout のようなグローバルエンドセッション URL を返します。 この URL は、ユーザーが現在ログインしているすべてのアプリケーションからユーザーをログアウトします。 ただし、現在のアプリケーションで特定のアプリケーションのみからユーザーをログアウトさせる必要がある場合は、quarkus.oidc.end-session-path=logout パラメーターを設定することで、グローバルエンドセッション URL をオーバーライドできます。

OIDC プロバイダーのクライアント認証

OIDC プロバイダーは通常、OIDC エンドポイントとやり取りする際に、アプリケーションの識別と認証を行う必要があります。 Quarkus OIDC、特に quarkus.oidc.runtime.OidcProviderClient クラスは、認可コードを ID、アクセス、リフレッシュトークンと交換する必要がある場合、または ID やアクセストークンをリフレッシュまたはイントロスペクトする必要がある場合に、OIDC プロバイダーで認証します。

通常、クライアント ID とクライアントシークレットは、アプリケーション毎に OIDC プロバイダーに登録される際に定義されます。 すべての OIDC クライアント認証 オプションがサポートされています。 例:

client_secret_basic の例:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=mysecret

あるいは:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret

次の例は、クレデンシャルプロバイダー から取得したシークレットを示しています。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app

# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc.credentials.client-secret.provider.name=oidc-credentials-provider
client_secret_post の例
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
quarkus.oidc.credentials.client-secret.method=post
client_secret_jwt の例。署名アルゴリズムは HS256:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
client_secret_jwt の例。この場合、シークレットは クレデンシャルプロバイダー から取得されます。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app

# This is a key which will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc.credentials.jwt.secret-provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc.credentials.jwt.secret-provider.name=oidc-credentials-provider

Example of private_key_jwt with the PEM key inlined in application.properties, and where the signature algorithm is RS256:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key=Base64-encoded private key representation

PEM 鍵ファイルを使用した private_key_jwt の例。署名アルゴリズムは RS256:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-file=privateKey.pem
keystoreファイルによる private_key_jwt の例。署名アルゴリズムはRS256:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-store-file=keystore.pkcs12
quarkus.oidc.credentials.jwt.key-store-password=mypassword
quarkus.oidc.credentials.jwt.key-password=mykeypassword

# Private key alias inside the keystore
quarkus.oidc.credentials.jwt.key-id=mykeyAlias

client_secret_jwt、または private_key_jwt 認証方法を使用することで、クライアントシークレットが OIDC プロバイダーに送信されないため、「中間者」攻撃によってシークレットが傍受されるリスクを回避できます。

JWT 認証の追加オプション

client_secret_jwtprivate_key_jwt、または Apple の post_jwt 認証方法が使用されている場合、JWT 署名アルゴリズム、キー識別子、audience、subject、および issuer をカスタマイズすることができます。 例:

# private_key_jwt client authentication

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus/
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.jwt.key-file=privateKey.pem

# This is a token key identifier 'kid' header - set it if your OIDC provider requires it:
# Note if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then
# using 'quarkus.oidc.credentials.jwt.token-key-id' is not necessary.
quarkus.oidc.credentials.jwt.token-key-id=mykey

# Use RS512 signature algorithm instead of the default RS256
quarkus.oidc.credentials.jwt.signature-algorithm=RS512

# The token endpoint URL is the default audience value, use the base address URL instead:
quarkus.oidc.credentials.jwt.audience=${quarkus.oidc-client.auth-server-url}

# custom subject instead of the client id:
quarkus.oidc.credentials.jwt.subject=custom-subject

# custom issuer instead of the client id:
quarkus.oidc.credentials.jwt.issuer=custom-issuer

Apple POST JWT

Apple OIDC プロバイダーは、private_key_jwt 認証方式で生成された JWT をシークレットとする client_secret_post 方式を使用していますが、Apple アカウント固有の issuer と subject のクレームを使用しています。

Quarkus Security では、quarkus-oidc は、非標準の client_secret_post_jwt 認証方法をサポートしています。この認証方法は、以下のように設定できます。

# Apple provider configuration sets a 'client_secret_post_jwt' authentication method
quarkus.oidc.provider=apple

quarkus.oidc.client-id=${apple.client-id}
quarkus.oidc.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc.credentials.jwt.token-key-id=${apple.key-id}
# Apple provider configuration sets ES256 signature algorithm

quarkus.oidc.credentials.jwt.subject=${apple.subject}
quarkus.oidc.credentials.jwt.issuer=${apple.issuer}

相互 TLS (mTLS)

OIDC プロバイダーによっては、相互 TLS 認証プロセスの一部としてクライアントの認証を要求する場合があります。

次の例は、mTLS をサポートするように quarkus-oidc を設定する方法を示しています。

quarkus.oidc.tls.tls-configuration-name=oidc

# configure hostname verification if necessary
#quarkus.tls.oidc.hostname-verification-algorithm=NONE

# Keystore configuration
quarkus.tls.oidc.key-store.p12.path=client-keystore.p12
quarkus.tls.oidc.key-store.p12.password=${key-store-password}

# Add more keystore properties if needed:
#quarkus.tls.oidc.key-store.p12.alias=keyAlias
#quarkus.tls.oidc.key-store.p12.alias-password=keyAliasPassword

# Truststore configuration
quarkus.tls.oidc.trust-store.p12.path=client-truststore.p12
quarkus.tls.oidc.trust-store.p12.password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.tls.oidc.trust-store.p12.alias=certAlias

POST クエリー

一部のプロバイダー ( Strava OAuth2 プロバイダーなど ) では、クライアントのクレデンシャルを HTTP POST クエリーパラメーターとして投稿する必要があります。

quarkus.oidc.provider=strava
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.client-secret.value=mysecret
quarkus.oidc.credentials.client-secret.method=query

イントロスペクションエンドポイント認証

一部の OIDC プロバイダーでは、Basic 認証と client_id および client_secret とは異なるクレデンシャルを使用して、イントロスペクションエンドポイントへの認証を行う必要があります。 OIDCプロバイダークライアント認証 セクションに記載されているように、以前にセキュリティー認証を設定して、client_secret_basic または client_secret_post クライアント認証メソッドをサポートしていた場合は、次のように追加の設定を適用することを推奨します。

トークンをイントロスペクションする必要があり、イントロスペクションエンドポイント固有の認証メカニズムが必要な場合は、quarkus-oidc を以下のように設定できます。

quarkus.oidc.introspection-credentials.name=introspection-user-name
quarkus.oidc.introspection-credentials.secret=introspection-user-secret

OIDC リクエストフィルター

1 つまたは複数の OidcRequestFilter 実装を登録することで、Quarkus から OIDC プロバイダーへの OIDC リクエストをフィルタリングすることが出来、リクエストヘッダーを更新または追加したり、リクエストをログに記録したりすることができます。

例:

package io.quarkus.it.keycloak;

import io.quarkus.oidc.OidcConfigurationMetadata;
import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContext;
import io.quarkus.oidc.common.OidcRequestFilter;

@ApplicationScoped
@Unremovable
public class OidcTokenRequestCustomizer implements OidcRequestFilter {
    @Override
    public void filter(OidcRequestContext requestContext) {
        OidcConfigurationMetadata metadata = requestContext.contextProperties().get(OidcConfigurationMetadata.class.getName()); (1)
        // Metadata URI is absolute, request URI value is relative
        if (metadata.getTokenUri().endsWith(requestContext.request().uri())) { (2)
            requestContext.request().putHeader("TokenGrantDigest", calculateDigest(requestContext.requestBody().toString()));
        }
    }
    private String calculateDigest(String bodyString) {
        // Apply the required digest algorithm to the body string
    }
}
1 サポートされているすべての OIDC エンドポイントアドレスが含まれる OidcConfigurationMetadata を取得します。
2 OidcConfigurationMetadata を使用して、OIDC トークンエンドポイントへのリクエストのみをフィルターします。

Alternatively, you can use an @OidcEndpoint annotation to apply this filter to responses from the OIDC discovery endpoint only:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContext;
import io.quarkus.oidc.common.OidcRequestFilter;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY) (1)
public class OidcDiscoveryRequestCustomizer implements OidcRequestFilter {

    @Override
    public void filter(OidcRequestContext requestContext) {
        requestContext.request().putHeader("Discovery", "OK");
    }
}
1 このフィルターを、OIDC 検出エンドポイントのみを対象とするリクエストに制限します。

OidcRequestContextProperties can be used to access request properties. Currently, you can use a tenand_id key to access the OIDC tenant id and a grant_type key to access the grant type which the OIDC provider uses to acquire tokens. The grant_type can only be set to either authorization_code or refresh_token grant type, when requests are made to the token endpoint. It is null in all other cases.

OIDC response filters

You can filter responses from the OIDC providers by registering one or more OidcResponseFilter implementations, which can check the response status, headers and body in order to log them or perform other actions.

You can have a single filter intercepting all the OIDC responses, or use an @OidcEndpoint annotation to apply this filter to the specific endpoint responses only. For example:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import org.jboss.logging.Logger;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcUtils;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN) (1)
public class TokenEndpointResponseFilter implements OidcResponseFilter {
    private static final Logger LOG = Logger.getLogger(TokenResponseFilter.class);

    @Override
    public void filter(OidcResponseContext rc) {
        String contentType = rc.responseHeaders().get("Content-Type"); (2)
        if (contentType.equals("application/json")
                && OidcConstants.AUTHORIZATION_CODE.equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) (3)
                && "code-flow-user-info-cached-in-idtoken".equals(rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE)) (3)
                && rc.responseBody().toJsonObject().containsKey("id_token")) { (4)
            LOG.debug("Authorization code completed for tenant 'code-flow-user-info-cached-in-idtoken'");
        }
    }
}
1 Restrict this filter to requests targeting the OIDC token endpoint only.
2 Check the response Content-Type header.
3 Use OidcRequestContextProperties request properties to check only an authorization_code token grant response for the code-flow-user-info-cached-in-idtoken tenant.
4 Confirm the response JSON contains an id_token property.

OIDC プロバイダーへのリダイレクトと OIDC プロバイダーからのリダイレクト

ユーザーが認証のために OIDC プロバイダーにリダイレクトされる場合、リダイレクト URL には redirect_uri クエリーパラメーターが含まれます。このパラメーターは、認証完了後のユーザーのリダイレクト先をプロバイダーに示します。 ここでのリダイレクト先は Quarkus アプリケーションになります。

Quarkus は、デフォルトでこのパラメーターを現在のアプリケーションリクエスト URL に設定します。 たとえば、ユーザーが http://localhost:8080/service/1 にある Quarkus サービスエンドポイントにアクセスしようとしている場合、redirect_uri パラメーターは http://localhost:8080/service/1 に設定されます。 同様に、リクエスト URL が http://localhost:8080/service/2 の場合、redirect_uri パラメーターは http://localhost:8080/service/2 に設定されます。

一部の OIDC プロバイダーでは、すべてのリダイレクト URL の特定のアプリケーションに対して、redirect_uri が同じ値 (例: http://localhost:8080/service/callback) を持つことが必要です。 このような場合は、quarkus.oidc.authentication.redirect-path プロパティーを設定する必要があります。 たとえば、quarkus.oidc.authentication.redirect-path=/service/callback の場合、Quarkus は redirect_uri パラメーターを http://localhost:8080/service/callback などの絶対 URL に設定します。これは、現在のリクエスト URL に関係なく同じになります。

quarkus.oidc.authentication.redirect-path が設定されているが、ユーザーが http://localhost:8080/service/callback などの一意のコールバック URL にリダイレクトされた後に元のリクエスト URL を復元する必要がある場合は、quarkus.oidc.authentication.restore-path-after-redirect プロパティーを true に設定します。 これにより、http://localhost:8080/service/1 などのリクエスト URL が復元されます。

認証リクエストのカスタマイズ

デフォルトでは、ユーザーが認証のために OIDC プロバイダーの認可エンドポイントにリダイレクトされる際に、response_type (code に設定)、scope (openid に設定)、client_idredirect_uri、および state プロパティーのみが HTTP クエリーパラメーターとして、OIDC プロバイダーの認可エンドポイントに渡されます。

quarkus.oidc.authentication.extra-params を使用して、さらにプロパティーを追加できます。 たとえば、一部の OIDC プロバイダーは、リダイレクト URI のフラグメントの一部として認可コードを返すことを選択する場合があり、これにより認証プロセスが中断されます。 次の例は、この問題を回避する方法を示しています。

quarkus.oidc.authentication.extra-params.response_mode=query

See also the OIDC redirect filters section explaining how a custom OidcRedirectFilter can be used to customize OIDC redirects, including those to the OIDC authorization endpoint.

認証エラーレスポンスのカスタマイズ

ユーザーが OIDC 認可エンドポイントにリダイレクトされ、Quarkus アプリケーションを認証し、必要に応じて認可する場合、このリダイレクトリクエストは、リダイレクト URI に無効なスコープが含まれている場合などに失敗する可能性があります。 このような場合、プロバイダーは、想定される code パラメーターではなく、error および error_description パラメーターを使用してユーザーを Quarkus にリダイレクトします。

これは、たとえば、プロバイダーへのリダイレクトに無効なスコープまたはその他の無効なパラメーターが含まれている場合に発生する可能性があります。

このような場合、デフォルトで HTTP 401 エラーが返されます。 ただし、ユーザーにわかりやすいエラーメッセージを返すために、カスタムパブリックエラーエンドポイントを呼び出すようにリクエストできます。 これを行うには、quarkus.oidc.authentication.error-path プロパティーを以下のように設定します。

quarkus.oidc.authentication.error-path=/error

プロパティーがフォワードスラッシュ (/) 文字で始まり、パスが現在のエンドポイントのベース URI と相対的であるようにします。 たとえば、これが '/error' に設定され、現在のリクエスト URI が https://localhost:8080/callback?error=invalid_scope の場合、最終的に https://localhost:8080/error?error=invalid_scope へリダイレクトされます。

ユーザーがこのページにリダイレクトされて再認証されないようにするには、このエラーエンドポイントがパブリックリソースであるようにします。

OIDC redirect filters

You can register one or more io.quarkus.oidc.OidcRedirectFilter implementations to filter OIDC redirects to OIDC authorization and logout endpoints but also local redirects to custom error and session expired pages. Custom OidcRedirectFilter can add additional query parameters, response headers and set new cookies.

For example, the following simple custom OidcRedirectFilter adds an additional query parameter and a custom response header for all redirect requests that can be done by Quarkus OIDC:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcRedirectFilter;

@ApplicationScoped
@Unremovable
public class GlobalOidcRedirectFilter implements OidcRedirectFilter {

    @Override
    public void filter(OidcRedirectContext context) {
        if (context.redirectUri().contains("/session-expired-page")) {
            context.additionalQueryParams().add("redirect-filtered", "true,"); (1)
            context.routingContext().response().putHeader("Redirect-Filtered", "true"); (2)
        }
    }

}
1 Add an additional query parameter. Note the queury names and values are URL-encoded by Quarkus OIDC, a redirect-filtered=true%20C query parameter is added to the redirect URI in this case.
2 Add a custom HTTP response header.

See also the 認証リクエストのカスタマイズ section how to configure additional query parameters for OIDC authorization point.

Custom OidcRedirectFilter for local error and session expired pages can also create secure cookies to help with generating such pages.

For example, let’s assume you need to redirect the current user whose session has expired to a custom session expired page available at http://localhost:8080/session-expired-page. The following custom OidcRedirectFilter encrypts the user name in a custom session_expired cookie using an OIDC tenant client secret:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import org.eclipse.microprofile.jwt.Claims;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcRedirectFilter;
import io.quarkus.oidc.Redirect;
import io.quarkus.oidc.Redirect.Location;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.jwt.build.Jwt;

@ApplicationScoped
@Unremovable
@TenantFeature("tenant-refresh")
@Redirect(Location.SESSION_EXPIRED_PAGE) (1)
public class SessionExpiredOidcRedirectFilter implements OidcRedirectFilter {

    @Override
    public void filter(OidcRedirectContext context) {

        if (context.redirectUri().contains("/session-expired-page")) {
        AuthorizationCodeTokens tokens = context.routingContext().get(AuthorizationCodeTokens.class.getName()); (2)
        String userName = OidcUtils.decodeJwtContent(tokens.getIdToken()).getString(Claims.preferred_username.name()); (3)
        String jwe = Jwt.preferredUserName(userName).jwe()
                .encryptWithSecret(context.oidcTenantConfig().credentials.secret.get()); (4)
        OidcUtils.createCookie(context.routingContext(), context.oidcTenantConfig(), "session_expired",
                jwe + "|" + context.oidcTenantConfig().tenantId.get(), 10); (5)
     }
    }
}
1 Make sure this redirect filter is only called during a redirect to the session expired page.
2 Access AuthorizationCodeTokens tokens associated with the now expired session as a RoutingContext attribute.
3 Decode ID token claims and get a user name.
4 Save the user name in a JWT token encrypted with the current OIDC tenant’s client secret.
5 Create a custom session_expired cookie valid for 5 seconds which joins the encrypted token and a tenant id using a "|" separator. Recording a tenant id in a custom cookie can help to generate correct session expired pages in a multi-tenant OIDC setup.

Next, a public JAX-RS resource which generates session expired pages can use this cookie to create a page tailored for this user and the corresponding OIDC tenant, for example:

package io.quarkus.it.keycloak;

import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

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

import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.runtime.OidcUtils;
import io.quarkus.oidc.runtime.TenantConfigBean;
import io.smallrye.jwt.auth.principal.DefaultJWTParser;
import io.vertx.ext.web.RoutingContext;

@Path("/session-expired-page")
public class SessionExpiredResource {
    @Inject
    RoutingContext context;

    @Inject
    TenantConfigBean tenantConfig; (1)

    @GET
    public String sessionExpired(@CookieParam("session_expired") String sessionExpired) throws Exception {
        // Cookie format: jwt|<tenant id>

        String[] pair = sessionExpired.split("\\|"); (2)
        OidcTenantConfig oidcConfig = tenantConfig.getStaticTenantsConfig().get(pair[1]).getOidcTenantConfig(); (3)
        JsonWebToken jwt = new DefaultJWTParser().decrypt(pair[0], oidcConfig.credentials.secret.get()); (4)
        OidcUtils.removeCookie(context, oidcConfig, "session_expired"); (5)
        return jwt.getClaim(Claims.preferred_username) + ", your session has expired. "
                + "Please login again at http://localhost:8081/" + oidcConfig.tenantId.get(); (6)
    }
}
1 Inject TenantConfigBean which can be used to access all the current OIDC tenant configurations.
2 Split the custom cookie value into 2 parts, first part is the encrypted token, last part is the tenant id.
3 Get the OIDC tenant configuration.
4 Decrypt the cookie value using the OIDC tenant’s client secret.
5 Remove the custom cookie.
6 Use the username in the decrypted token and the tenant id to generate the service expired page response.

認可データへのアクセス

認可に関する情報には、さまざまな方法でアクセスできます。

ID およびアクセストークンへのアクセス

OIDC コード認証メカニズムは、認可コードフロー中に 3 つのトークン IDトークン、アクセストークン、リフレッシュトークンを取得します。

ID トークンは常に JWT トークンであり、JWT クレームによるユーザー認証を表します。 これを使って発行元の OIDC エンドポイントやユーザー名、その他 クレーム と呼ばれる情報を取得することができます。 JsonWebTokenIdToken という修飾子をつけることで、ID トークンのクレームにアクセスすることができます。

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;

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

    @Inject
    @IdToken
    JsonWebToken idToken;

    @GET
    public String getUserName() {
        return idToken.getName();
    }
}

OIDC web-app アプリケーションは通常、アクセストークンを使用して、現在ログインしているユーザーに代わって他のエンドポイントにアクセスします。 raw アクセストークンには次のようにアクセスできます。

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.security.Authenticated;

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

    @Inject
    JsonWebToken accessToken;

    // or
    // @Inject
    // AccessTokenCredential accessTokenCredential;

    @GET
    public String getReservationOnBehalfOfUser() {
        String rawAccessToken = accessToken.getRawToken();
        //or
        //String rawAccessToken = accessTokenCredential.getToken();

        // Use the raw access token to access a remote endpoint.
        // For example, use RestClient to set this token as a `Bearer` scheme value of the HTTP `Authorization` header:
        // `Authorization: Bearer rawAccessToken`.
        return getReservationfromRemoteEndpoint(rawAccesstoken);
    }
}

When an authorization code flow access token is injected as JsonWebToken, its verification is automatically enabled, in addition to the mandatory ID token verification. If really needed, you can disable this code flow access token verification with quarkus.oidc.authentication.verify-access-token=false.

AccessTokenCredential は、Quarkus web-app アプリケーションに発行されたアクセストークンが不透明 (バイナリー) で、JsonWebToken に解析できない場合、または内部コンテンツがアプリケーションに必要な場合に使用されます。

JsonWebTokenAccessTokenCredential の注入は、@RequestScoped@ApplicationScoped の両方のコンテキストでサポートされています。

Quarkus OIDC は、セッション管理 プロセスの一環として、リフレッシュトークンを使用し、現在の ID とアクセストークンを更新します。

ユーザー情報

ID トークンが現在認証されているユーザーに関する十分な情報を提供しない場合は、UserInfo エンドポイントから詳細情報を取得できます。 quarkus.oidc.authentication.user-info-required=true プロパティーを設定して、OIDC UserInfo エンドポイントから UserInfo JSON オブジェクトをリクエストします。

認可コードの付与応答で返されたアクセストークンを使用して、OIDC プロバイダーの UserInfo エンドポイントにリクエストが送信され、io.quarkus.oidc.UserInfo (単純な jakarta.json.JsonObject ラッパー) オブジェクトが作成されます。 io.quarkus.oidc.UserInfo は、SecurityIdentity userinfo 属性として注入またはアクセスできます。

quarkus.oidc.authentication.user-info-required is automatically enabled if one of these conditions is met:

  • if quarkus.oidc.roles.source is set to userinfo or quarkus.oidc.token.verify-access-token-with-user-info is set to true or quarkus.oidc.authentication.id-token-required is set to false, the current OIDC tenant must support a UserInfo endpoint in these cases.

  • if io.quarkus.oidc.UserInfo injection point is detected but only if the current OIDC tenant supports a UserInfo endpoint.

OIDC 設定情報へのアクセス

現在のテナントの検出された OpenID Connect 設定メタデータ は、io.quarkus.oidc.OidcConfigurationMetadata で表され、SecurityIdentity configuration-metadata 属性として注入またはアクセスできます。

エンドポイントがパブリックの場合、デフォルトのテナントの OidcConfigurationMetadata が注入されます。

トークンクレームと SecurityIdentity ロールのマッピング

検証済みトークンから SecurityIdentity ロールにロールをマッピングする方法は、ベアラートークン の方法と同じです。 唯一の違いは、 ID トークン がデフォルトでロールのソースとして使用されることです。

Keycloak を使用する場合は、ID トークンに groups クレームが含まれるように microprofile-jwt クライアントスコープを設定します。 詳細は、Keycloak サーバー管理ガイド のリンクを参照してください。

ただし、OIDC プロバイダーによっては、ロールがアクセストークンまたはユーザー情報に保存される場合があります。

アクセストークンにロールが含まれており、このアクセストークンがダウンストリームエンドポイントに伝播されることを意図していない場合は、quarkus.oidc.roles.source=accesstoken を設定します。

UserInfo がロールのソースである場合は、quarkus.oidc.roles.source=userinfo を設定し、必要に応じて quarkus.oidc.roles.role-claim-path を設定します。

さらに、カスタム SecurityIdentityAugmentor を使用してロールを追加することもできます。 詳細は、SecurityIdentity カスタマイズ を参照してください。 また、HTTP Security ポリシー を使用して、トークン要求から作成された SecurityIdentity ロールをデプロイメント固有のロールにマップすることもできます。

トークンと認証データの有効性の確認

認証プロセスの中核となるのは、トラストチェーンと情報の有効性を確認することです。 これは、トークンが信頼できることを確認することによって行われます。

トークン検証およびイントロスペクション

The verification process of OIDC authorization code flow tokens follows the Bearer token authentication token verification and introspection logic. For more information, see the Token verification and introspection section of the "Quarkus OpenID Connect (OIDC) Bearer token authentication" guide.

Quarkus の web-app アプリケーションでは、アクセストークンは現在の Quarkus の web-app エンドポイントへのアクセスには使用されず、このアクセストークンを必要とするサービスに伝播されることが意図されているため、デフォルトでは IdToken のみが検証されます。 アクセストークンに現在の Quarkus エンドポイント (quarkus.oidc.roles.source=accesstoken) にアクセスするために必要なロールが含まれていることが予想される場合は、それも検証されます。

トークンのイントロスペクションと UserInfo のキャッシュ

コードフローアクセストークンは、ロールのソースであると期待されない限り、イントロスペクトされません。 ただし、アクセストークンは UserInfo を取得するために使用されます。 トークンイントロスペクション または UserInfo、あるいはその両方が必要な場合は、コードフローアクセストークンを使用したリモート呼び出しが 1 回または 2 回発生します。

For more information about using the default token cache or registering a custom cache implementation, see Token introspection and UserInfo cache.

JSON Web トークンのクレーム検証

For information about the claim verification, including the iss (issuer) claim, see the JSON Web Token claim verification section. It applies to ID tokens and also to access tokens in a JWT format, if the web-app application has requested the access token verification.

Jose4j Validator

You can register a custom [Jose4j Validator] to customize the JWT claim verification process. See Jose4j section for more information.

Proof Key for Code Exchange (PKCE)

Proof Key for Code Exchange (PKCE) は、認可コードの傍受のリスクを最小限に抑えます。

PKCE は、ブラウザーで実行される SPA スクリプトなどのパブリック OIDC クライアントにとって最も重要ですが、Quarkus OIDC web-app アプリケーションに追加の保護を提供することもできます。 PKCE を使用すると、Quarkus OIDC web-app アプリケーションは、クライアントシークレットを安全に保存し、それを使用してトークンのコードを交換できる機密 OIDC クライアントとして機能します。

次の例に示すように、quarkus.oidc.authentication.pkce-required プロパティーと、stateクッキーの PKCE コード検証を暗号化するために必要な 32 文字のシークレットを使用して、OIDC web-app エンドポイントの PKCE を有効にすることができます。

quarkus.oidc.authentication.pkce-required=true
quarkus.oidc.authentication.state-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU

すでに 32 文字のクライアントシークレットがある場合は、別のシークレットキーを使用する場合を除き、quarkus.oidc.authentication.pkce-secret プロパティーを設定する必要はありません。 このシークレットは、設定されていない場合、およびクライアントシークレットの長さが 16 文字未満でクライアントシークレットへのフォールバックが不可能な場合に、自動生成されます。

シークレットキーは、ランダムに生成された PKCE code_verifier を暗号化するために必要です。一方、ユーザーは code_challenge クエリーパラメーターを使用して OIDC プロバイダーにリダイレクトされ、認証されます。 code_verifier は、ユーザーが Quarkus にリダイレクトされるときに復号化され、code、クライアントシークレット、およびその他のパラメーターとともにトークンエンドポイントに送信され、コード交換が完了します。 code_verifierSHA256 ダイジェストが、認証のリクエスト時に提供された code_challenge と一致しない場合、プロバイダーはコード交換に失敗します。

認証のライフタイムの処理と制御

認証のもう 1 つの重要な要件は、ユーザーがリクエストのたびに認証をリクエストすることなく、セッションの基となるデータが最新であることを保証することです。 また、ログアウトイベントが明示的にリクエストされる状況もあります。 以下のポイントを参考に、Quarkus アプリケーションのセキュリティーを確保するための適切なバランスを見つけて下さい。

クッキー

OIDC アダプターはクッキーを使用して、セッション、コードフロー、ログアウト後の状態を保持します。 この状態は、認証データの寿命を制御する重要な要素です。

quarkus.oidc.authentication.cookie-path プロパティーを使用すると、保護されたリソースに重複または異なるルートでアクセスしたときに、同じクッキーが表示されるようになります。 例:

  • /index.html/web-app/service

  • /web-app/service1/web-app/service2

  • /web-app1/service/web-app2/service

デフォルトでは、quarkus.oidc.authentication.cookie-path/ に設定されていますが、必要に応じてこれを /web-app などのより具体的なパスに変更できます。

To set the cookie path dynamically, configure the quarkus.oidc.authentication.cookie-path-header property. For example, to set the cookie path dynamically by using the value of the X-Forwarded-Prefix HTTP header, configure the property to quarkus.oidc.authentication.cookie-path-header=X-Forwarded-Prefix.

quarkus.oidc.authentication.cookie-path-header が設定されているが、現在のリクエストで設定された HTTP ヘッダーが利用できない場合は、quarkus.oidc.authentication.cookie-path がチェックされます。

If your application is deployed across multiple domains, set the quarkus.oidc.authentication.cookie-domain property so that the session cookie is visible to all protected Quarkus services. For example, if you have Quarkus services deployed on the following two domains, then you must set the quarkus.oidc.authentication.cookie-domain property to company.net:

  • https://whatever.wherever.company.net/

  • https://another.address.company.net/

State cookies

State cookies are used to support authorization code flow completion. When an authorization code flow is started, Quarkus creates a state cookie and a matching state query parameter, before redirecting the user to the OIDC provider. When the user is redirected back to Quarkus to complete the authorization code flow, Quarkus expects that the request URI must contain the state query parameter and it must match the current state cookie value.

The default state cookie age is 5 mins and you can change it with a quarkus.oidc.authentication.state-cookie-age Duration property.

Quarkus creates a unique state cookie name every time a new authorization code flow is started to support multi-tab authentication. Many concurrent authentication requests on behalf of the same user may cause a lot of state cookies be created. If you do not want to allow your users use multiple browser tabs to authenticate then it is recommended to disable it with quarkus.oidc.authentication.allow-multiple-code-flows=false. It also ensures that the same state cookie name is created for every new user authentication.

セッションクッキーとデフォルトの TokenStateManager

OIDC CodeAuthenticationMechanism は、デフォルトの io.quarkus.oidc.TokenStateManager インターフェイス実装を使用して、認可コードまたはリフレッシュ付与応答で返された ID、アクセストークン、およびリフレッシュトークンを暗号化されたセッションクッキーに保存します。

これにより、Quarkus OIDC エンドポイントは完全にステートレスになり、最高のスケーラビリティー結果を達成するには、このストラテジーに従うことが推奨されます。

Refer to the Database TokenStateManager section of this guide for information on storing tokens in the database or other server-side storage solutions. This approach is suitable if you prefer and have compelling reasons to store the token state on the server.

See the セッションクッキーとカスタムTokenStateManager section for alternative methods of token storage. This is ideal for those seeking customized solutions for token state management, especially when standard server-side storage does not meet your specific requirements.

デフォルトの TokenStateManager を設定すると、アクセストークンをセッションクッキーに保存せずに、ID トークンとリフレッシュトークンのみ、または単一の ID トークンのみを保持できます。

アクセストークンは、エンドポイントが次のアクションを実行する必要がある場合にのみ必要です。

  • UserInfo の取得

  • このアクセストークンを使用したダウンストリームサービスへのアクセス

  • デフォルトでチェックされるアクセストークンに関連付けられたロールの使用

このような場合は、quarkus.oidc.token-state-manager.strategy プロパティーを使用して、トークン状態ストラテジーを次のように設定します。

以下の場合 プロパティーを以下に設定する

ID とリフレッシュトークンのみを保存する場合

quarkus.oidc.token-state-manager.strategy=id-refresh-tokens

ID トークンのみを保存する場合

quarkus.oidc.token-state-manager.strategy=id-token

選択したセッションクッキーストラテジーがトークンを組み合わせ、4 KB を超える大きなセッションクッキー値が生成される場合、一部のブラウザーではそのようなクッキーサイズを処理できない可能性があります。 これは、ID、アクセストークン、およびリフレッシュトークンが JWT トークンで、選択されたストラテジーが keep-all-tokens の場合、またはストラテジーが id-refresh-token の場合にID トークンとリフレッシュトークンで発生する可能性があります。 この問題を回避するには、quarkus.oidc.token-state-manager.split-tokens=true を設定して、トークンごとに一意のセッショントークンを作成します。 別の解決策として、トークンをデータベースに保存することが挙げられます。 詳細は、Database TokenStateManager を参照してください。

デフォルトの TokenStateManager は、トークンをセッションクッキーに保存する前に暗号化します。 次の例は、トークンを分割して暗号化するように設定する方法を示しています。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=quarkus-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app
quarkus.oidc.token-state-manager.split-tokens=true
quarkus.oidc.token-state-manager.encryption-secret=eUk1p7UB3nFiXZGUXi0uph1Y9p34YhBU

トークンの暗号化シークレットは 32 文字以上である必要があります。 このキーが設定されていない場合は、quarkus.oidc.credentials.secret または quarkus.oidc.credentials.jwt.secret のいずれかがハッシュ化されて暗号化キーが作成されます。

Quarkus が次のいずれかの認証方法を使用して OIDC プロバイダーに対して認証する場合は、quarkus.oidc.token-state-manager.encryption-secret プロパティーを設定してください。

  • mTLS

  • private_key_jwt では、秘密の RSA または EC キーを使用して JWT トークンに署名します。

これ以外の場合は、ランダムなキーが生成されますが、Quarkus アプリケーションがクラウドで実行され、複数の Pod がリクエストを管理している場合、問題が発生する可能性があります。

quarkus.oidc.token-state-manager.encryption-required=false を設定することで、セッションクッキーでのトークン暗号化を無効にすることができます。

セッションクッキーとカスタムTokenStateManager

If you want to customize the way the tokens are associated with the session cookie, register a custom io.quarkus.oidc.TokenStateManager implementation as an @ApplicationScoped CDI bean.

たとえば、トークンをキャッシュクラスターに保持し、キーのみをセッションクッキーに保存することを推奨します。 トークンを複数のマイクロサービスノードで利用できるようにする必要がある場合、このアプローチではいくつかの課題が生じる可能性があることに注意してください。

簡単な例を挙げてみます。

package io.quarkus.oidc.test;

import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Inject;

import io.quarkus.oidc.AuthorizationCodeTokens;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenStateManager;
import io.quarkus.oidc.runtime.DefaultTokenStateManager;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
@Alternative
@Priority(1)
public class CustomTokenStateManager implements TokenStateManager {

    @Inject
    DefaultTokenStateManager tokenStateManager;

    @Override
    public Uni<String> createTokenState(RoutingContext routingContext, OidcTenantConfig oidcConfig,
            AuthorizationCodeTokens sessionContent, OidcRequestContext<String> requestContext) {
        return tokenStateManager.createTokenState(routingContext, oidcConfig, sessionContent, requestContext)
                .map(t -> (t + "|custom"));
    }

    @Override
    public Uni<AuthorizationCodeTokens> getTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig,
            String tokenState, OidcRequestContext<AuthorizationCodeTokens> requestContext) {
        if (!tokenState.endsWith("|custom")) {
            throw new IllegalStateException();
        }
        String defaultState = tokenState.substring(0, tokenState.length() - 7);
        return tokenStateManager.getTokens(routingContext, oidcConfig, defaultState, requestContext);
    }

    @Override
    public Uni<Void> deleteTokens(RoutingContext routingContext, OidcTenantConfig oidcConfig, String tokenState,
            OidcRequestContext<Void> requestContext) {
        if (!tokenState.endsWith("|custom")) {
            throw new IllegalStateException();
        }
        String defaultState = tokenState.substring(0, tokenState.length() - 7);
        return tokenStateManager.deleteTokens(routingContext, oidcConfig, defaultState, requestContext);
    }
}

暗号化されたセッションクッキーにトークンを保存するデフォルトの TokenStateManager の詳細は、セッションクッキーとデフォルトの TokenStateManager を参照してください。

トークンをデータベースに保存するカスタム Quarkus TokenStateManager 実装の詳細は、Database TokenStateManager を参照してください。

Database TokenStateManager

ステートフルなトークンストレージストラテジーを採用したい場合は、Quarkus が提供するカスタムの TokenStateManager を使用して、アプリケーションがトークンを暗号化されたセッションクッキーに保存するのではなく、データベースに保存することができます。これは、セッションクッキーとデフォルトの TokenStateManager セクションに記載されているとおり、デフォルトで設定されています。

この機能を使用するには、以下のエクステンションをプロジェクトに追加します。

コマンドラインインタフェース
quarkus extension add oidc-db-token-state-manager
Maven
./mvnw quarkus:add-extension -Dextensions='oidc-db-token-state-manager'
Gradle
./gradlew addExtension --extensions='oidc-db-token-state-manager'

This extension will replace the default io.quarkus.oidc.TokenStateManager with a database-based one.

OIDC Database Token State Manager は、認証が IO スレッドで行われる可能性が高いため、ブロックを回避するために内部で Reactive SQL クライアントを使用します。

データベースに応じて、Reactive SQL クライアント を 1 つだけ含めて設定します。 次の Reactive SQL クライアントがサポートされています。

  • Reactive Microsoft SQL クライアント

  • Reactive MySQL クライアント

  • Reactive PostgreSQL クライアント

  • Reactive Oracle クライアント

  • Reactive DB2 クライアント

アプリケーションがすでにいずれかの JDBC ドライバーエクステンションを備えた Hibernate ORM を使用している場合は、Reactive SQL クライアントを使用するように切り替える必要はありません。

たとえば、Hibernate ORM エクステンションと PostgreSQL JDBC ドライバーを併用するアプリケーションがすでにあり、データソースが次のように設定されているとします。

quarkus.datasource.db-kind=postgresql
quarkus.datasource.username=quarkus_test
quarkus.datasource.password=quarkus_test
quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/quarkus_test

ここで、OIDC Database Token State Manager を使用することにした場合は、次の依存関係を追加し、リアクティブドライバー URL を設定する必要があります。

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-db-token-state-manager</artifactId>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-reactive-pg-client</artifactId>
</dependency>
build.gradle
implementation("io.quarkus:quarkus-oidc-db-token-state-manager")
implementation("io.quarkus:quarkus-reactive-pg-client")
quarkus.datasource.reactive.url=postgresql://localhost:5432/quarkus_test

これで、トークンをデータベースに保存する準備が整いました。

デフォルトでは、トークンの保存に使用されるデータベーステーブルが作成されますが、quarkus.oidc.db-token-state-manager.create-database-table-if-not-exists 設定プロパティーを使用して、このオプションを無効にすることができます。 代わりに Hibernate ORM エクステンションでこのテーブルを作成する場合は、次のように Entity を含める必要があります。

package org.acme.manager;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Table(name = "oidc_db_token_state_manager") (1)
@Entity
public class OidcDbTokenStateManagerEntity {

    @Id
    String id;

    @Column(name = "id_token", length = 4000) (2)
    String idToken;

    @Column(name = "refresh_token", length = 4000)
    String refreshToken;

    @Column(name = "access_token", length = 4000)
    String accessToken;

    @Column(name = "expires_in")
    Long expiresIn;
}
1 Hibernate ORM エクステンションは、データベーススキーマが生成された場合にのみ、このテーブルを作成します。 詳細は、Hibernate ORM ガイドを参照してください。
2 トークンの長さに応じて列の長さを選択できます。

ログアウトと有効期限

認証情報が期限切れになる主な方法は 2 つあります。トークンの有効期限が切れて更新されなかった場合と、明示的なログアウト操作がトリガーされた場合です。

最初に、明示的なログアウト操作について説明します。

User-initiated logout

Users can request a logout by sending a request to the Quarkus endpoint logout path set with a quarkus.oidc.logout.path property. For example, if the endpoint address is https://application.com/webapp and the quarkus.oidc.logout.path is set to /logout, then the logout request must be sent to https://application.com/webapp/logout.

このログアウトリクエストは、 RP-initiated ログアウト を開始します。 ユーザはログアウトするために OIDC プロバイダーにリダイレクトされ、そこでログアウトが本当に意図されたものであるか確認されます。

The user will be returned to the endpoint post-logout page once the logout has been completed and if the quarkus.oidc.logout.post-logout-path property is set. For example, if the endpoint address is https://application.com/webapp and the quarkus.oidc.logout.post-logout-path is set to /signin, then the user will be returned to https://application.com/webapp/signin. Note, this URI must be registered as a valid post_logout_redirect_uri in the OIDC provider.

quarkus.oidc.logout.post-logout-path が設定されている場合、q_post_logout クッキーが作成され、一致する state クエリーパラメーターがログアウトリダイレクト URI に追加され、ログアウトが完了すると OIDC プロバイダーはこの state を返します。 Quarkus の web-app アプリケーションでは、state クエリーパラメーターが q_post_logout クッキーの値と一致することを確認することを推奨します。これは、たとえば Jakarta REST フィルターで実行できます。

OpenID Connect Multi-Tenancy を使用する場合、クッキー名が異なることに注意してください。 たとえば、tenant_1 ID を持つテナントの場合は q_post_logout_tenant_1 という名前になります。

ログアウトフローを開始するように Quarkus アプリケーションを設定する方法の例を次に示します。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app

quarkus.oidc.logout.path=/logout
# Logged-out users should be returned to the /welcome.html site which will offer an option to re-login:
quarkus.oidc.logout.post-logout-path=/welcome.html

# Only the authenticated users can initiate a logout:
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated

# All users can see the Welcome page:
quarkus.http.auth.permission.public.paths=/welcome.html
quarkus.http.auth.permission.public.policy=permit

また、quarkus.oidc.authentication.cookie-path をすべてのアプリケーションリソースに共通のパス値 (この例では /) に設定することも推奨します。 詳細は、Cookies セクションを参照してください。

一部の OIDC プロバイダーは、RP-initiated ログアウト 仕様をサポートしておらず、OpenID Connect の周知の(well-knownの) end_session_endpoint メタデータプロパティーを返しません。 ただし、このような OIDC プロバイダーの特定のログアウトメカニズムは、ログアウト URL クエリーパラメーターの命名方法のみが異なるため、Quarkus にとってはこれは問題ではありません。

RP-initiated ログアウト 仕様によれば、quarkus.oidc.logout.post-logout-path プロパティーは post_logout_redirect_uri クエリーパラメーターとして表されますが、この仕様をサポートしていないプロバイダーでは認識されません。

この問題を回避するには、quarkus.oidc.logout.post-logout-url-param を使用できます。 quarkus.oidc.logout.extra-params で追加されたログアウトクエリーパラメーターをさらにリクエストすることもできます。 たとえば、Auth0 でログアウトをサポートする方法は次のとおりです。

quarkus.oidc.auth-server-url=https://dev-xxx.us.auth0.com
quarkus.oidc.client-id=redacted
quarkus.oidc.credentials.secret=redacted
quarkus.oidc.application-type=web-app

quarkus.oidc.tenant-logout.logout.path=/logout
quarkus.oidc.tenant-logout.logout.post-logout-path=/welcome.html

# Auth0 does not return the `end_session_endpoint` metadata property. Instead, you must configure it:
quarkus.oidc.end-session-path=v2/logout
# Auth0 will not recognize the 'post_logout_redirect_uri' query parameter so ensure it is named as 'returnTo':
quarkus.oidc.logout.post-logout-uri-param=returnTo

# Set more properties if needed.
# For example, if 'client_id' is provided, then a valid logout URI should be set as the Auth0 Application property, without it - as Auth0 Tenant property:
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}

バックチャネルログアウト

OIDC プロバイダーは、認証データを使用して、すべてのアプリケーションを強制的にログアウトさせることができます。 これはバックチャネルログアウトとして知られています。 この場合、OIDC は各アプリケーションから特定の URL を呼び出し、ログアウトをトリガーします。

OIDC プロバイダーは、バックチャネルログアウト を使用して、ユーザエージェントをバイパスして、そのユーザが現在ログインしているすべてのアプリケーションから現在のユーザをログアウトします。

バックチャネルログアウトをサポートするように Quarkus を設定するには、次のようにします。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app

quarkus.oidc.logout.backchannel.path=/back-channel-logout

絶対的な back-channel logout URL は、現在のエンドポイント URL に quarkus.oidc.back-channel-logout.path を追加することによって計算されます (例: http://localhost:8080/back-channel-logout)。 この URL は、OIDC プロバイダーの管理コンソールで設定する必要があります。

OIDC プロバイダーが、現在のログアウトトークンに有効期限を設定していない場合、ログアウトトークンの検証が成功するようにトークンの有効期間プロパティーを設定する必要もあります。 たとえば、ログアウトトークンの iat (発行時刻) から 10 秒超過しないようにするには、quarkus.oidc.token.age=10S を設定します。

フロントチャネルログアウト

フロントチャネルログアウト のリンクを使用すると、ブラウザーなどのユーザーエージェントから現在のユーザーを直接ログアウトできます。 これは バックチャネルログアウト と似ていますが、ログアウト手順はブラウザーなどのユーザーエージェントによって実行され、OIDC プロバイダーがバックグラウンドで実行することはありません。 これは、ほとんど使用されないオプションです。

Quarkus でフロントチャネルのログアウトをサポートするには、以下のように設定します。

quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=frontend
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app

quarkus.oidc.logout.frontchannel.path=/front-channel-logout

このパスは現在のリクエストのパスと比較され、これらのパスがマッチする 場合、ユーザはログアウトします。

ローカルログアウト

User-initiated logout は、ユーザーを OIDC プロバイダーからログアウトします。 これをシングルサインオンとして使用する場合は、必要とするものではない可能性があります。 たとえば、OIDC プロバイダーが Google の場合、Google とそのサービスからログアウトされます。 代わりに、ユーザーはその特定のアプリケーションからログアウトしたいだけかもしれません。 もう 1 つのユースケースとしては、OIDC プロバイダーにログアウトエンドポイントがない場合が考えられます。

OidcSession を使用することで、ローカルログアウトをサポートできます。つまり、次の例に示すように、ローカルセッションクッキーのみがクリアされます。

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.OidcSession;

@Path("/service")
public class ServiceResource {

    @Inject
    OidcSession oidcSession;

    @GET
    @Path("logout")
    public String logout() {
        oidcSession.logout().await().indefinitely();
        return "You are logged out";
    }
}

OidcSession をローカルログアウトに使用

io.quarkus.oidc.OidcSession は現在の IdToken のラッパーで、Local logout の実行、現行セッションのテナント識別子の取得、セッションの有効期限の確認を行う上で役に立ちます。 今後、より便利なメソッドが追加される予定です。

セッション管理

デフォルトでは、ログアウトは OIDC プロバイダーによって発行された ID トークンの有効期限に基づいて行われます。 ID トークンの有効期限が切れると、Quarkus エンドポイントでの現在のユーザーセッションは無効になり、ユーザーは認証のために再度 OIDC プロバイダーにリダイレクトされます。 OIDC プロバイダーのセッションが引き続き有効な場合、ユーザーは再度クレデンシャルを入力することなく、自動的に再認証されます。

quarkus.oidc.token.refresh-expired プロパティーを有効にすると、現在のユーザーセッションを自動的に延長できます。 true に設定すると、現在の ID トークンの有効期限が切れたときに、リフレッシュトークンの付与を使用して、ID トークンだけでなくアクセストークンとリフレッシュトークンもリフレッシュされます。

keycloak.js などの OIDC プロバイダースクリプトが認可コードフローを管理している サービスアプリケーション用のシングルページアプリケーション がある場合、そのスクリプトは SPA 認証セッションの有効期間も制御します。

Quarkus OIDC web-app アプリケーションを使用する場合、Quarkus OIDC コード認証メカニズムがユーザーセッションの有効期間を管理します。

リフレッシュトークンを使用するには、セッションクッキーの有効期間を慎重に設定する必要があります。 セッションの有効期間は、ID トークンの有効期間よりも長く、リフレッシュトークンの有効期間と近いか等しい必要があります。

セッションの有効期間は、現在の ID トークンの有効期間の値と、quarkus.oidc.authentication.session-age-extension プロパティーおよび quarkus.oidc.token.lifespan-grace プロパティーの値を加算して計算します。

必要に応じて、quarkus.oidc.authentication.session-age-extension プロパティーのみを使用して、セッションの有効期間を大幅に延長します。 quarkus.oidc.token.lifespan-grace プロパティーは、小さなクロックスキューを考慮する場合にのみ使用します。

現在認証されているユーザーが、保護された Quarkus エンドポイントに戻り、セッションクッキーに関連付けられた ID トークンの有効期限が切れると、デフォルトでは、ユーザーは再認証のために OIDC 認可エンドポイントに自動的にリダイレクトされます。 ユーザーとこの OIDC プロバイダー間のセッションがまだアクティブな場合、OIDC プロバイダーはユーザーに再度チャレンジする可能性があります。これは、セッションが ID トークンよりも長く続くように設定されている場合に発生する可能性があります。

quarkus.oidc.token.refresh-expiredtrue に設定されている場合、期限切れの ID トークン (およびアクセストークン) は、初期認可コード付与応答で返されたリフレッシュトークンを使用して更新されます。 このリフレッシュトークンも、このプロセスの一環としてリサイクル (リフレッシュ) される可能性があります。 その結果、新しいセッションクッキーが作成され、セッションが延長されます。

ユーザーがあまりアクティブでない場合は、quarkus.oidc.authentication.session-age-extension プロパティーを使用して、期限切れの ID トークンを処理できます。 ID トークンの有効期限が切れると、クッキーの有効期間が経過するため、次のユーザーリクエスト時にセッションクッキーが Quarkus エンドポイントに返されない可能性があります。 Quarkus は、このリクエストが最初の認証リクエストであると想定します。 quarkus.oidc.authentication.session-age-extension を、ほとんどアクティブでないユーザーとセキュリティーポリシーに合わせて、適度に 長い値に設定します。

さらに一歩進んで、期限切れが近づいている ID トークンまたはアクセストークンを事前に更新することもできます。 quarkus.oidc.token.refresh-token-time-skew を、更新を予測する値に設定します。 現在のユーザーリクエスト中に、現在の ID トークンがこの quarkus.oidc.token.refresh-token-time-skew 内に期限切れになると計算された場合、トークンは更新され、新しいセッションクッキーが作成されます。 このプロパティーは、ID トークンの有効期間よりも短い値に設定する必要があります。この有効期間の値に近いほど、ID トークンの更新頻度が高くなります。

単純な JavaScript 関数を使用して Quarkus エンドポイントに定期的に ping を送信し、ユーザーアクティビティーをエミュレートすることで、このプロセスをさらに最適化できます。これにより、ユーザーが再認証される必要がある時間枠が最小限に抑えられます。

When the session can not be refreshed, the currently authenticated user is redirected to the OIDC provider to re-authenticate. However, the user experience may not be ideal in such cases, if the user, after an earlier successful authentication, is suddently seeing an OIDC authentication challenge screen when trying to access an application page.

Instead, you can request that the user is redirected to a public, application specific session expired page first. This page informs the user that the session has now expired and advise to re-authenticate by following a link to a secured application welcome page. The user clicks on the link and Quarkus OIDC enforces a redirect to the OIDC provider to re-authenticate. Use quarkus.oidc.authentication.session-expired-page relative path property, if you’d like to do it.

For example, setting quarkus.oidc.authentication.session-expired-page=/session-expired-page will ensure that the user whose session has expired is redirected to http://localhost:8080/session-expired-page, assuming the application is available at http://localhost:8080.

See also the OIDC redirect filters section explaining how a custom OidcRedirectFilter can be used to customize OIDC redirects, including those to the session expired pages.

このユーザーセッションを無期限に延長することはできません。 有効期限が切れた ID トークンを持つ復帰ユーザーは、リフレッシュトークンの有効期限が切れると、OIDC プロバイダーエンドポイントで再認証する必要があります。

GitHub および OIDC 以外の OAuth2 プロバイダーとのインテグレーション

GitHub や LinkedIn のような有名なプロバイダーは OpenID Connect プロバイダーではなく、authorization code flow をサポートする OAuth2 プロバイダーです。 たとえば、GitHub OAuth2LinkedIn OAuth2 などです。 OIDC は OAuth2 の上に構築されていることを思い出してください。

OIDC プロバイダと OAuth2 プロバイダの主な違いは、OIDC プロバイダは OAuth2 プロバイダが返す標準認可コードフロー access および refresh トークンに加えて、ユーザ認証を表す ID Token を返すことです。

GitHub などの OAuth2 プロバイダは IdToken を返さないため、ユーザー認証は access トークンによって暗黙的かつ間接的に表現されます。 この access トークンは、現在のQuarkus web-app アプリケーションが認証されたユーザーに代わってデータにアクセスすることを認可する、認証されたユーザーを表します。

For OIDC, you validate the ID token as proof of authentication validity whereas in the case of OAuth2, you validate the access token. This is done by subsequently calling an endpoint that requires the access token and that typically returns user information. This approach is similar to the OIDC UserInfo approach, with UserInfo fetched by Quarkus OIDC on your behalf.

たとえば、GitHub と連携する場合、Quarkus エンドポイントは access トークンを取得できます。これにより、Quarkus エンドポイントは現在のユーザーの GitHub プロファイルをリクエストできます。

このような OAuth2 サーバーとのインテグレーションをサポートするには、quarkus-oidc を少し異なる方法で設定して、IdToken: quarkus.oidc.authentication.id-token-required=false なしで認可コードフローの応答を許可する必要があります。

Even though you configure the extension to support the authorization code flows without IdToken, an internal IdToken is generated to standardize the way quarkus-oidc operates. You use an internal IdToken to support the authentication session and to avoid redirecting the user to the provider, such as GitHub, on every request. In this case, the IdToken age is set to the value of a standard expires_in property in the authorization code flow response. You can use a quarkus.oidc.authentication.internal-id-token-lifespan property to customize the ID token age. The default ID token age is 5 minutes, which you can extend further as described in the session management section.

これにより、複数の OIDC プロバイダーをサポートするアプリケーションの取り扱いが簡単になります。

The next step is to ensure that the returned access token can be useful and is valid to the current Quarkus endpoint. The first way is to call the OAuth2 provider introspection endpoint by configuring quarkus.oidc.introspection-path, if the provider offers such an endpoint. In this case, you can use the access token as a source of roles using quarkus.oidc.roles.source=accesstoken. If no introspection endpoint is present, you can attempt instead to request UserInfo from the provider as it will at least validate the access token. To do so, specify quarkus.oidc.token.verify-access-token-with-user-info=true. You also need to set the quarkus.oidc.user-info-path property to a URL endpoint that fetches the user info (or to an endpoint protected by the access token). For GitHub, since it does not have an introspection endpoint, requesting the UserInfo is required.

Requiring UserInfo involves making a remote call on every request.

Therefore, UserInfo is embedded in the internal generated IdToken and saved in the encrypted session cookie. It can be disabled with quarkus.oidc.cache-user-info-in-idtoken=false.

Alternatively, you might want to consider caching UserInfo using a default or custom UserInfo cache provider. For more information, see the Token Introspection and UserInfo cache section of the "OpenID Connect (OIDC) Bearer token authentication" guide.

Most well-known social OAuth2 providers enforce rate-limiting so there is a high chance you will prefer to have UserInfo cached.

OAuth2 サーバーは、よく知られた設定エンドポイントをサポートしていない可能性があります。 この場合、検出を無効にして、認可、トークン、イントロスペクション、および UserInfo エンドポイントパスを手動で設定する必要があります。

For well-known OIDC or OAuth2 providers, such as Apple, Facebook, GitHub, Google, Microsoft, Spotify, and X (formerly Twitter), Quarkus can help significantly simplify your application’s configuration with the quarkus.oidc.provider property. Here is how you can integrate quarkus-oidc with GitHub after you have created a GitHub OAuth application. Configure your Quarkus endpoint like this:

quarkus.oidc.provider=github
quarkus.oidc.client-id=github_app_clientid
quarkus.oidc.credentials.secret=github_app_clientsecret

# user:email scope is requested by default, use 'quarkus.oidc.authentication.scopes' to request different scopes such as `read:user`.
# See https://docs.github.com/en/developers/apps/building-oauth-apps/scopes-for-oauth-apps for more information.

# Consider enabling UserInfo Cache
# quarkus.oidc.token-cache.max-size=1000
# quarkus.oidc.token-cache.time-to-live=5M
#
# Or having UserInfo cached inside IdToken itself
# quarkus.oidc.cache-user-info-in-idtoken=true

他のよく知られているプロバイダーの設定に関する詳細は、OpenID Connect プロバイダー を参照してください。

このようなエンドポイントに対して必要なのは、現在認証されているユーザーのプロファイルを GET http://localhost:8080/github/userinfo で返し、個々の UserInfo のプロパティーとしてアクセスすることです。

package io.quarkus.it.keycloak;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;

@Path("/github")
@Authenticated
public class TokenResource {

    @Inject
    UserInfo userInfo;

    @GET
    @Path("/userinfo")
    @Produces("application/json")
    public String getUserInfo() {
        return userInfo.getUserInfoString();
    }
}

OpenID Connect Multi-Tenancy を使用して複数のソーシャルプロバイダー (たとえば、IdToken を返す OIDC プロバイダーの Google と、IdToken を返さず UserInfo へのアクセスのみを許可する OAuth2 プロバイダーの GitHub) をサポートする場合、Google フローと GitHub フローの両方で、注入された SecurityIdentity のみを使用してエンドポイントを動作させることができます。 GitHub フローがアクティブな場合に、内部で生成された IdToken で作成されたプリンシパルが UserInfo ベースのプリンシパルに置き換えられる場合は、SecurityIdentity の簡単な拡張が必要になります。

package io.quarkus.it.keycloak;

import java.security.Principal;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.UserInfo;
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 io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomSecurityIdentityAugmentor implements SecurityIdentityAugmentor {

    @Override
    public Uni<SecurityIdentity> augment(SecurityIdentity identity, AuthenticationRequestContext context) {
        RoutingContext routingContext = identity.getAttribute(RoutingContext.class.getName());
        if (routingContext != null && routingContext.normalizedPath().endsWith("/github")) {
	        QuarkusSecurityIdentity.Builder builder = QuarkusSecurityIdentity.builder(identity);
	        UserInfo userInfo = identity.getAttribute("userinfo");
	        builder.setPrincipal(new Principal() {

	            @Override
	            public String getName() {
	                return userInfo.getString("preferred_username");
	            }

	        });
	        identity = builder.build();
        }
        return Uni.createFrom().item(identity);
    }

}

これで、ユーザーが Google または GitHub を使用してアプリケーションにサインインすると、次のコードが機能するようになります。

package io.quarkus.it.keycloak;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;

@Path("/service")
@Authenticated
public class TokenResource {

    @Inject
    SecurityIdentity identity;

    @GET
    @Path("/google")
    @Produces("application/json")
    public String getGoogleUserName() {
        return identity.getPrincipal().getName();
    }

    @GET
    @Path("/github")
    @Produces("application/json")
    public String getGitHubUserName() {
        return identity.getPrincipal().getName();
    }
}

おそらく、より簡単な代替案は、@IdToken JsonWebTokenUserInfo の両方を注入し、IdToken を返すプロバイダーを処理する際は JsonWebToken を使用して、IdToken を返さないプロバイダーの場合は UserInfo を使用することです。

You must ensure that the callback path you enter in the GitHub OAuth application configuration matches the endpoint path where you want the user to be redirected after a successful GitHub authentication and application authorization. In this case, it has to be set to http://localhost:8080/github/userinfo.

重要な認証イベントのリッスン

重要な OIDC 認証イベントを監視する @ApplicationScoped Bean を登録できます。 ユーザが初めてログインしたり、再認証したり、セッションをリフレッシュしたりすると、リスナーが更新されます。 将来的には、さらに多くのイベントが報告されるようになるかもしれません。 例:

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;

import io.quarkus.oidc.SecurityEvent;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class SecurityEventListener {

    public void event(@Observes SecurityEvent event) {
        String tenantId = event.getSecurityIdentity().getAttribute("tenant-id");
        RoutingContext vertxContext = event.getSecurityIdentity().getAttribute(RoutingContext.class.getName());
        vertxContext.put("listener-message", String.format("event:%s,tenantId:%s", event.getEventType().name(), tenantId));
    }
}
「セキュリティのヒントとコツ」ガイドの セキュリティイベントの監視 セクションで説明されているように、他のセキュリティイベントを聞くことができます。

下流サービスへのトークンの伝播

For information about Authorization Code Flow access token propagation to downstream services, see the Token Propagation section.

インテグレーションに関する考慮事項

OIDC によって保護されたアプリケーションは、シングルページアプリケーションから呼び出すことができる環境に統合されます。 これは、よく知られている OIDC プロバイダーと連携し、HTTP リバースプロキシーの背後で実行される必要があるほか、外部および内部アクセスなども必要とされます。

このセクションでは、これらの考慮事項について説明します。

シングルページアプリケーション

「OpenID Connect (OIDC) ベアラートークン認証」ガイドの シングルページアプリケーション セクションで提案されている方法でシングルページアプリケーション (SPA) を実装した場合、要件が満たされるか確認できます。

Quarkus Web アプリケーションで FetchXMLHttpRequest(XHR) などの SPA および JavaScript API を使用する場合は、Quarkus からのリダイレクト後にユーザーが認証される認可エンドポイントに対して、OIDC プロバイダーがクロスオリジンリソース共有 (CORS) をサポートしない可能性があることに注意してください。 Quarkus アプリケーションと OIDC プロバイダーが異なる HTTP ドメイン、ポート、またはその両方でホストされている場合、認証に失敗します。

このような場合は、quarkus.oidc.authentication.java-script-auto-redirect プロパティーを false に設定します。これにより、Quarkus は 499 ステータスコードと OIDC 値を含む WWW-Authenticate ヘッダーを返すように指示されます。

quarkus.oidc.authentication.java-script-auto-redirect プロパティーが false に設定されている場合に 499 ステータスコードが返されるようにするには、ブラウザースクリプトで現在のリクエストを JavaScript リクエストとして識別するためのヘッダーを設定する必要があります。

スクリプトエンジンがエンジン固有のリクエストヘッダーを設定する場合は、カスタム quarkus.oidc.JavaScriptRequestChecker Bean を登録できます。これにより、現在のリクエストが JavaScript リクエストであるかどうかが Quarkus に通知されます。たとえば、JavaScript エンジンが HX-Request: true などのヘッダーを設定する場合は、次のようにチェックできます。

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.oidc.JavaScriptRequestChecker;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomJavaScriptRequestChecker implements JavaScriptRequestChecker {

    @Override
    public boolean isJavaScriptRequest(RoutingContext context) {
        return "true".equals(context.request().getHeader("HX-Request"));
    }
}

ステータスコードが 499 の場合は、最後にリクエストされたページを再度読み込みます。

それ以外の場合は、ブラウザースクリプトを更新し、X-Requested-With ヘッダーに JavaScript 値を設定して、499 ステータスコードの場合は最後にリクエストされたページを再度読み込みする必要があります。

例:

Future<void> callQuarkusService() async {
    Map<String, String> headers = Map.fromEntries([MapEntry("X-Requested-With", "JavaScript")]);

    await http
        .get("https://localhost:443/serviceCall")
        .then((response) {
            if (response.statusCode == 499) {
                window.location.assign("https://localhost.com:443/serviceCall");
            }
         });
  }

クロスオリジンリソース共有

別のドメインで実行されているシングルページのアプリケーションからこのアプリケーションを利用する場合は、クロスオリジンリソース共有 (CORS) を設定する必要があります。 詳細は、「クロスオリジンリソース共有」ガイドの CORSフィルター のセクションを参照してください。

クラウドプロバイダーサービスの呼び出し

Google Cloud

Google Developer Consoles の BigQuery などの Google Cloud Services に対して OIDC 認可コードフロー権限を有効化している現在認証済みのユーザーに代わり、Quarkus OIDC web-app アプリケーションが、それらのサービスにアクセスできるようにすることが可能です。

Quarkiverse Google クラウドサービス を使用して、これを行うことができます。 追加する必要があるのは、 以下の例に示すように、 最新タグ サービス依存関係のみです。

pom.xml
<dependency>
    <groupId>io.quarkiverse.googlecloudservices</groupId>
    <artifactId>quarkus-google-cloud-bigquery</artifactId>
    <version>${quarkiverse.googlecloudservices.version}</version>
</dependency>
build.gradle
implementation("io.quarkiverse.googlecloudservices:quarkus-google-cloud-bigquery:${quarkiverse.googlecloudservices.version}")

次に、Google OIDC プロパティーを設定します。

quarkus.oidc.provider=google
quarkus.oidc.client-id={GOOGLE_CLIENT_ID}
quarkus.oidc.credentials.secret={GOOGLE_CLIENT_SECRET}
quarkus.oidc.token.issuer=https://accounts.google.com

Quarkus アプリケーションのリバースプロキシーの背後での実行

Quarkus アプリケーションがリバースプロキシー、ゲートウェイ、またはファイアウォールの背後で実行されている場合に、HTTP Host ヘッダーが内部 IP アドレスにリセットされたり、HTTPS 接続が終端されたりするなどして、OIDC 認証メカニズムが影響を受けることがあります。 たとえば、認可コードフローの redirect_uri パラメーターが、予期される外部ホストではなく内部ホストに設定されている場合があります。

このような場合、プロキシーによって転送された元のヘッダーを認識するように Quarkus を設定する必要があります。 詳細は、リバースプロキシーの背後での実行 Vert.x のドキュメントセクションを参照してください。

たとえば、Quarkus エンドポイントが Kubernetes Ingress の背後にあるクラスターで実行されている場合、計算された redirect_uri パラメーターが内部エンドポイントアドレスを指している可能性があるため、OIDC プロバイダーからこのエンドポイントへのリダイレクトが機能しない可能性があります。 この問題は、Kubernetes Ingress によって外部エンドポイントアドレスを表すように X-ORIGINAL-HOST が設定されている次の設定を使用することで解決できます。

quarkus.http.proxy.proxy-address-forwarding=true
quarkus.http.proxy.allow-forwarded=false
quarkus.http.proxy.enable-forwarded-host=true
quarkus.http.proxy.forwarded-host-header=X-ORIGINAL-HOST

quarkus.oidc.authentication.force-redirect-https-scheme プロパティーは、Quarkus アプリケーションが SSL 終了リバースプロキシーの背後で実行されている場合にも使用できます。

OIDC プロバイダーへの外部および内部アクセス

OIDC プロバイダーの外部からアクセス可能な認可、ログアウト、およびその他のエンドポイントは、自動検出された URL や内部 URL quarkus.oidc.auth-server-url とは異なる HTTP(S) URL を持つ場合があります。 このような場合、エンドポイントは発行者の検証の失敗を報告し、外部からアクセス可能な OIDC プロバイダーエンドポイントへのリダイレクトが失敗する可能性があります。

Keycloak を使用する場合は、KEYCLOAK_FRONTEND_URL システムプロパティーを外部からアクセス可能なベース URL に設定して起動します。 他の OIDC プロバイダーと連携する場合は、プロバイダーのドキュメントを確認してください。

OIDC SAML アイデンティティーブローカー

アイデンティティープロバイダーが OpenID Connect を実装しておらず、従来の XML ベースの SAML2.0 SSO プロトコルのみを実装している場合、quarkus-oidc を OIDC アダプターとして使用する場合と同じように、Quarkus を SAML 2.0 アダプターとして使用することはできません。

ただし、Keycloak、Okta、Auth0、Microsoft ADFS などの多くの OIDC プロバイダーは、OIDC から SAML 2.0 へのブリッジを提供しています。 OIDC プロバイダーで SAML 2.0 プロバイダーへのアイデンティティーブローカー接続を作成し、quarkus-oidc を使用してこの SAML 2.0 プロバイダーに対してユーザーを認証し、OIDC プロバイダーが OIDC と SAML 2.0 の通信を調整することができます。 Quarkus エンドポイントに関しては、同じ Quarkus セキュリティー、OIDC API、@AuthenticatedSecurityIdentity などのアノテーションなどを引き続き使用できます。

たとえば、Okta が SAML 2.0 プロバイダーで、Keycloak が OIDC プロバイダーだとします。 ここでは、KeycloakOkta SAML 2.0 プロバイダーと仲介するように設定する方法を説明する一般的なシーケンスを示します。

まず、Okta Dashboard/Applications に新しい SAML2 インテグレーションを作成します。

Okta Create SAML Integration

たとえば、OktaSaml と名前を付けます。

Okta SAML General Settings

Next, configure it to point to a Keycloak SAML broker endpoint. At this point, you need to know the name of the Keycloak realm, for example, quarkus, and, assuming that the Keycloak SAML broker alias is saml, enter the endpoint address as http://localhost:8081/realms/quarkus/broker/saml/endpoint. Enter the service provider (SP) entity ID as http://localhost:8081/realms/quarkus, where http://localhost:8081 is a Keycloak base address and saml is a broker alias:

Okta SAML Configuration

次に、この SAML インテグレーションを保存し、その Metadata URL をメモします。

Okta SAML Metadata

続いて、SAML プロバイダーを Keycloak に追加します。

まず、通常どおりに、新しいレルムを作成するか、既存のレルムを Keycloak にインポートします。 この場合、レルム名は quarkus にする必要があります。

次に、quarkus レルムのプロパティーで、Identity Providers に移動し、新しい SAML プロバイダーを追加します。

Keycloak Add SAML Provider

Note the alias is set to saml, Redirect URI is http://localhost:8081/realms/quarkus/broker/saml/endpoint and Service provider entity ID is http://localhost:8081/realms/quarkus - these are the same values you entered when creating the Okta SAML integration in the previous step.

最後に、前の手順の最後に書き留めた Okta SAML Integration Metadata URL を指すように Service entity descriptor を設定します。

次に、必要に応じて、 Authentication/browser/Identity Provider Redirector config に移動し、Alias プロパティーと Default Identity Provider プロパティーの両方を saml に設定して、この Keycloak SAML プロバイダーをデフォルトプロバイダーとして登録できます。 デフォルトのプロバイダーとして設定しない場合は、認証時に Keycloak は次の 2 つのオプションを提供します。

  • SAML プロバイダーによる認証

  • 名前とパスワードを使用した Keycloak への直接認証

ここで、Quarkus OIDC web-app アプリケーションを、Keycloak quarkus レルム quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus を指すように設定します。 次に、Keycloak OIDC および Okta SAML 2.0 プロバイダーによって提供される OIDC から SAML へのブリッジを使用して、Quarkus ユーザーを Okta SAML 2.0 プロバイダーに認証する準備が整います。

Keycloak の場合と同様に、他の OIDC プロバイダーを設定して SAML ブリッジを提供することもできます。

テスト

別の OIDC のようなサーバーへの認証に関しては、テストが困難になることがよくあります。 Quarkus は、モックから OIDC プロバイダーのローカル実行まで、さまざまなオプションを提供します。

テストプロジェクトに以下の依存関係を追加することから始めます。

pom.xml
<dependency>
    <groupId>org.htmlunit</groupId>
    <artifactId>htmlunit</artifactId>
    <exclusions>
        <exclusion>
            <groupId>org.eclipse.jetty</groupId>
            <artifactId>*</artifactId>
       </exclusion>
    </exclusions>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
build.gradle
testImplementation("org.htmlunit:htmlunit")
testImplementation("io.quarkus:quarkus-junit5")

Wiremock

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

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

REST テストエンドポイントを準備し、application.properties を設定します。 以下に例を示します。

# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-web-app
quarkus.oidc.credentials.secret=secret
quarkus.oidc.application-type=web-app

最後にテストコードを書きます。例:

import static org.junit.jupiter.api.Assertions.assertEquals;

import org.junit.jupiter.api.Test;

import org.htmlunit.SilentCssErrorHandler;
import org.htmlunit.WebClient;
import org.htmlunit.html.HtmlForm;
import org.htmlunit.html.HtmlPage;

import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;

@QuarkusTest
@QuarkusTestResource(OidcWiremockTestResource.class)
public class CodeFlowAuthorizationTest {

    @Test
    public void testCodeFlow() throws Exception {
        try (final WebClient webClient = createWebClient()) {
            // the test REST endpoint listens on '/code-flow'
            HtmlPage page = webClient.getPage("http://localhost:8081/code-flow");

            HtmlForm form = page.getFormByName("form");
            // user 'alice' has the 'user' role
            form.getInputByName("username").type("alice");
            form.getInputByName("password").type("alice");

            page = form.getInputByValue("login").click();

            assertEquals("alice", page.getBody().asNormalizedText());
        }
    }

    private WebClient createWebClient() {
        WebClient webClient = new WebClient();
        webClient.setCssErrorHandler(new SilentCssErrorHandler());
        return webClient;
    }
}

OidcWiremockTestResourcealiceadmin ユーザーを認識します。 ユーザー alice にはデフォルトで user ロールしかありませんが、quarkus.test.oidc.token.user-roles システムプロパティーでカスタマイズできます。 ユーザー admin にはデフォルトで useradmin ロールがありますが、quarkus.test.oidc.token.user-roles システムプロパティーでカスタマイズできます。

さらに、OidcWiremockTestResource はトークンの発行者と対象ユーザーを https://service.example.com に設定します。これは、quarkus.test.oidc.token.issuer および quarkus.test.oidc.token.audience システムプロパティーを使用してカスタマイズできます。

OidcWiremockTestResource は、すべての OIDC プロバイダーをエミュレートするために使用できます。

Dev Services for Keycloak

Keycloak に対する結合テストには、Dev Services for Keycloak を使用することを推奨します。 Dev Services for Keycloak は、テストコンテナーを起動して初期化します。これにより、quarkus レルム、quarkus-app クライアント (secret シークレット) が作成され、alice (admin および user ロール) および bob (user ロール) ユーザーが追加されます。これらのプロパティーは、すべてカスタマイズできます。

まず、application.properties を準備します。 Dev Services for Keycloak は、実行中のテストコンテナーを指す quarkus.oidc.auth-server-url のほか、quarkus.oidc.client-id=quarkus-app および quarkus.oidc.credentials.secret=secret を登録するため、完全に空の application.properties ファイルから開始できます。

ただし、必要なすべての quarkus-oidc プロパティーがすでに設定されている場合は、quarkus.oidc.auth-server-urlDev Services for Keycloakprod プロファイルに関連付けるだけで、コンテナーを起動できます。 以下に例を示します。

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus

テストを実行する前に、カスタムレルムファイルを Keycloak にインポートする必要がある場合は、次のように Dev Services for Keycloak を設定できます。

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json

Finally, write a test code the same way as it is described in the Wiremock section. The only difference is that @QuarkusTestResource is no longer needed:

@QuarkusTest
public class CodeFlowAuthorizationTest {
}

KeecycloakTestResourceLifecycleManager の使用

Use KeycloakTestResourceLifecycleManager for your tests only if there is a good reason not to use Dev Services for Keycloak. If you need to do the integration testing against Keycloak then you are encouraged to do it with Dev Services for Keycloak.

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

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

これは、Keycloak コンテナーを起動する io.quarkus.test.common.QuarkusTestResourceLifecycleManager の実装である io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager を提供します。

次に、Maven Surefire プラグインを次のように設定します (ネイティブイメージでテストする場合は、Maven Failsafe プラグインを同様に設定します)。

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <systemPropertyVariables>
            <!-- or, alternatively, configure 'keycloak.version' -->
            <keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
            <!--
              Disable HTTPS if required:
              <keycloak.use.https>false</keycloak.use.https>
            -->
        </systemPropertyVariables>
    </configuration>
</plugin>

Now, set the configuration and write the test code the same way as it is described in the Wiremock section. The only difference is the name of QuarkusTestResource:

import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;

@QuarkusTest
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class CodeFlowAuthorizationTest {
}

KeycloakTestResourceLifecycleManageraliceadmin ユーザーを登録します。 ユーザー alice にはデフォルトで user ロールしかありませんが、keycloak.token.user-roles システムプロパティーでカスタマイズできます。 ユーザー admin にはデフォルトで useradmin ロールがありますが、keycloak.token.admin-roles システムプロパティーでカスタマイズできます。

デフォルトでは、KeycloakTestResourceLifecycleManager が HTTPS を使用して Keycloak インスタンスを初期化します。これは、keycloak.use.https=false を指定することで無効にすることができます。 デフォルトのレルム名は quarkus で、クライアント ID は quarkus-web-app です。必要に応じて、keycloak.realmkeycloak.web-app.client システムプロパティーを設定して値をカスタマイズしてください。

TestSecurity アノテーション

You can use @TestSecurity and @OidcSecurity annotations to test the web-app application endpoint code, which depends on either one of the following injections, or all four:

  • ID JsonWebToken

  • Access JsonWebToken

  • UserInfo

  • OidcConfigurationMetadata

ログでのエラー確認

トークン検証エラーの詳細を確認するには、io.quarkus.oidc.runtime.OidcProviderTRACE レベルのロギングを有効にする必要があります。

quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE

OidcProvider クライアントの初期化エラーの詳細を確認するには、io.quarkus.oidc.runtime.OidcRecorderTRACE レベルのロギングを有効にしてください。

quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE

アプリケーションのグローバルログレベルを変更するには、quarkus dev コンソールから j と入力します。

関連コンテンツ