OpenID Connect (OIDC) ベアラートークン認証
Quarkus OpenID Connect(OIDC)エクステンションを使用することで、ベアラートークン認証を使って、アプリケーション内のJakarta REST(旧称JAX-RS)エンドポイントへのHTTPアクセスを保護できます。
Quarkusのベアラートークン認証メカニズムの概要
Quarkusは、Quarkus OpenID Connect(OIDC)エクステンションを通じて、ベアラートークン認証メカニズムをサポートしています。
ベアラートークンは、 Keycloak などのOIDCやOAuth2.0に準拠した認可サーバーで発行されます。
ベアラートークン認証は、ベアラートークンの存在と有効性に基づいて HTTP リクエストを認可するプロセスです。 ベアラートークンは、呼び出しの主体に関する情報を提供します。この情報は、HTTP リソースにアクセスできるかどうかを判断するために使用されます。
以下の図に、Quarkusのベアラートークン認証メカニズムの概要を示します:

-
Quarkus サービスが、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
-
Quarkus ユーザーが、シングルページアプリケーション (SPA) にアクセスします。
-
シングルページアプリケーションが、認可コードフローを使用してユーザーを認証し、OIDC プロバイダーからトークンを取得します。
-
シングルページアプリケーションが、アクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
-
Quarkus サービスが、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックします。トークンが有効であればリクエストの続行を許可し、シングルページアプリケーションにサービスレスポンスを返します。
-
シングルページアプリケーションが、Quarkus ユーザーに同じデータを返します。

-
Quarkus サービスが、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
-
クライアントが、
client_credentials
かパスワードグラントを使用して、OIDC プロバイダーからアクセストークンを取得します。client_credentials には、クライアント ID とシークレットが必要です。パスワードグラントには、クライアント ID、シークレット、ユーザー名、およびパスワードが必要です。 -
クライアントがアクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
-
Quarkus サービスが、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックします。トークンが有効であればリクエストの続行を許可し、サービスレスポンスをクライアントに返します。
OIDC 認可コードフローを使用してユーザーを認証および認可する必要がある場合は、Quarkus の Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム ガイドを参照してください。 また、Keycloak とベアラートークンを使用する場合は、Quarkus Keycloak を使用した認可の一元化 ガイドを参照してください。
OIDC ベアラートークン認証を使用してサービスアプリケーションを保護する方法については、次のチュートリアルを参照してください。
マルチテナントをサポートする方法については、Quarkus OpenID Connect マルチテナンシーの使用 ガイドを参照してください。
JWT クレームへのアクセス
JWT トークンクレームにアクセスする必要がある場合は、 JsonWebToken
を注入できます。
package org.acme.security.openid.connect;
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.inject.Inject;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/api/admin")
public class AdminResource {
@Inject
JsonWebToken jwt;
@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "Access for subject " + jwt.getSubject() + " is granted";
}
}
JsonWebToken
の注入は、 @ApplicationScoped
、 @Singleton
、および @RequestScoped
スコープでサポートされています。
ただし、個々のクレームが単純型として挿入される場合は、 @RequestScoped
を使用する必要があります。
詳細は、Quarkus の「JWT RBAC の使用」ガイドの サポートされている注入スコープ セクションを参照してください。
UserInfo
OIDC UserInfo
エンドポイントから UserInfo JSON オブジェクトをリクエストする必要がある場合は、 quarkus.oidc.authentication.user-info-required=true
を設定します。
OIDC プロバイダーの UserInfo
エンドポイントにリクエストが送信され、 io.quarkus.oidc.UserInfo
(単純な javax.json.JsonObject
ラッパー) オブジェクトが作成されます。
io.quarkus.oidc.UserInfo
は、 SecurityIdentity
userinfo
属性として注入またはアクセスできます。
quarkus.oidc.authentication.user-info-required
は、次のいずれかの条件が満たされた場合に自動的に有効になります。
-
quarkus.oidc.roles.source
がuserinfo
に設定されている場合、またはquarkus.oidc.token.verify-access-token-with-user-info
がtrue
に設定されている場合、またはquarkus.oidc.authentication.id-token-required
がfalse
に設定されている場合。このような場合、現在の OIDC テナントが UserInfo エンドポイントをサポートしている必要があります。 -
io.quarkus.oidc.UserInfo
インジェクションポイントが検出された場合。ただし、有効になるのは、現在の OIDC テナントが UserInfo エンドポイントをサポートしている場合だけです。
設定メタデータ
現在のテナントの検出された OpenID Connect 設定メタデータ は、 io.quarkus.oidc.OidcConfigurationMetadata
で表され、 SecurityIdentity
configuration-metadata
属性として注入またはアクセスできます。
エンドポイントがパブリックの場合、デフォルトのテナントの OidcConfigurationMetadata
が注入されます。
トークンクレームと SecurityIdentity ロール
SecurityIdentity
ロールは、次のように、検証済みの JWT アクセストークンからマッピングすることができます。
-
quarkus.oidc.roles.role-claim-path
プロパティーが設定されており、一致する配列または文字列のクレームが見つかった場合、そのクレームからロールが抽出されます。 たとえば、customroles
、customroles/array
、scope
、"http://namespace-qualified-custom-claim"/roles
、"http://namespace-qualified-roles"
などです。 -
groups
クレームが利用可能な場合は、その値が使用されます。 -
realm_access/roles
またはresource_access/client_id/roles
(client_id
はquarkus.oidc.client-id
プロパティーの値) クレームが利用可能な場合は、その値が使用されます。 このチェックは、Keycloak が発行するトークンをサポートします。
たとえば、次の JWT トークンには、ロールが含まれている roles
配列を含む複雑な groups
クレームがあります。
{
"iss": "https://server.example.com",
"sub": "24400320",
"upn": "jdoe@example.com",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"groups": {
"roles": [
"microprofile_jwt_user"
],
}
}
microprofile_jwt_user
ロールは、 SecurityIdentity
ロールにマップする必要があります。これは、 quarkus.oidc.roles.role-claim-path=groups/roles
設定で実行できます。
トークンが不透明 (バイナリー) の場合は、リモートトークンイントロスペクションレスポンスの scope
プロパティーが使用されます。
UserInfo
がロールのソースである場合は、 quarkus.oidc.authentication.user-info-required=true
および quarkus.oidc.roles.source=userinfo
を設定し、必要に応じて quarkus.oidc.roles.role-claim-path
を設定します。
さらに、カスタムの SecurityIdentityAugmentor
を使用してロールを追加することもできます。
詳細は、Quarkus の「セキュリティーに関するヒントとコツ」ガイドの セキュリティーアイデンティティーのカスタマイズ セクションを参照してください。
また、HTTP セキュリティーポリシー を使用して、トークンクレームから作成された SecurityIdentity
ロールをデプロイメント固有のロールにマップすることもできます。
トークンのスコープと SecurityIdentity の権限
SecurityIdentity
権限は、 io.quarkus.security.StringPermission
の形式で、source of the roles のスコープパラメーターから、同じクレームセパレーターを使用してマッピングされます。
import java.util.List;
import jakarta.inject.Inject;
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.security.PermissionsAllowed;
@Path("/service")
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@PermissionsAllowed("email") (1)
@GET
@Path("/email")
public Boolean isUserEmailAddressVerifiedByUser() {
return accessToken.getClaim(Claims.email_verified.name());
}
@PermissionsAllowed("orders_read") (2)
@GET
@Path("/order")
public List<Order> listOrders() {
return List.of(new Order("1"));
}
public static class Order {
String id;
public Order() {
}
public Order(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId() {
this.id = id;
}
}
}
1 | OpenID Connect スコープ email を持つリクエストのみにアクセスが許可されます。 |
2 | 読み取りアクセスは、 orders_read スコープを持つクライアントリクエストに制限されます。 |
io.quarkus.security.PermissionsAllowed
アノテーションの詳細は、「Web エンドポイントの認可」ガイドの 権限アノテーション セクションを参照してください。
トークン検証およびイントロスペクション
トークンが JWT トークンの場合、デフォルトでは、OIDC プロバイダーの JWK エンドポイントから取得されたローカルの JsonWebKeySet
の JsonWebKey
(JWK) キーを使用してトークンが検証されます。
トークンのキー識別子 (kid
) ヘッダー値は、一致する JWK キーを検出するために使用されます。
一致する JWK
がローカルで利用できない場合は、JWK エンドポイントから現在のキーセットが取得され、 JsonWebKeySet
が更新されます。
JsonWebKeySet
の更新は、 quarkus.oidc.token.forced-jwk-refresh-interval
の有効期限が切れた後にのみ繰り返すことができます。
デフォルトの有効期限は 10 分です。
一致する JWK
が更新後に利用できない場合は、JWT トークンが OIDC プロバイダーのトークンイントロスペクションエンドポイントに送信されます。
トークンが不透明である場合、つまりバイナリートークンまたは暗号化された JWT トークンである場合、そのトークンは常に OIDC プロバイダーのトークンイントロスペクションエンドポイントに送信されます。
JWT トークンのみを使用しており、一致する JsonWebKey
が常に使用可能であることが予想される場合 (たとえば、キーセットを更新した後)、次の例に示すように、トークンイントロスペクションを無効にする必要があります。
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
場合によっては、JWT トークンをイントロスペクションのみで検証しなければならないことがあります。これは、イントロスペクションエンドポイントアドレスのみを設定することで強制できます。 次のプロパティー設定は、これを Keycloak を使用して実現する方法の例を示しています。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
JWT トークンのイントロスペクションをリモートで間接的に実施することには、利点と欠点があります。 利点は、2 つのリモート呼び出し (リモート OIDC メタデータ検出呼び出しと、それに続く使用されない検証キーを取得するための別のリモート呼び出し) が不要になることです。 欠点は、ユーザーがイントロスペクションエンドポイントアドレスを確認して、手動で設定する必要があることです。
別の方法としては、デフォルトオプションである OIDC メタデータ検出を許可しながら、リモート JWT イントロスペクションのみを実行するよう要求する方法があります。次の例を参照してください。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
この方法の利点は、設定がよりシンプルで理解しやすいことです。 欠点は、検証キーが取得されない場合でも、イントロスペクションエンドポイントアドレスを検出するためにリモート OIDC メタデータ検出呼び出しが必要になることです。
io.quarkus.oidc.TokenIntrospection
(シンプルな jakarta.json.JsonObject
ラッパーオブジェクト) が作成されます。
このオブジェクトは、JWT または不透明トークンどちらかのイントロスペクションが成功した場合、 SecurityIdentity
introspection
属性として注入またはアクセスできます。
トークンのイントロスペクションと UserInfo
キャッシュ
不透明アクセストークンは、すべてリモートでイントロスペクトする必要があります。
場合によっては、JWT アクセストークンをイントロスペクトする必要もあります。
UserInfo
も必要な場合は、OIDC プロバイダーへの後続のリモート呼び出しで同じアクセストークンが使用されます。
したがって、 UserInfo
が必要で、現在のアクセストークンが不透明トークンである場合、そのトークンごとに 2 つのリモート呼び出しが行われます。1 つのリモート呼び出しはトークンをイントロスペクトするためのもので、もう 1 つは UserInfo
を取得するためのものです。
トークンが JWT の場合は、トークンをイントロスペクトする必要がなければ、 UserInfo
を取得するための 1 つのリモート呼び出しだけで済みます。
場合によっては、着信ベアラーまたはコードフローアクセストークンごとに最大 2 つのリモート呼び出しを行うコストが問題になります。
これが本番環境に当てはまる場合は、トークンイントロスペクションと UserInfo
データを短期間 (たとえば 3 分から 5 分) キャッシュすることを検討してください。
quarkus-oidc
は、 quarkus.oidc.TokenIntrospectionCache
および quarkus.oidc.UserInfoCache
インターフェイスを提供します。
これらは @ApplicationScoped
キャッシュ実装に使用できます。次の例に示すように、 @ApplicationScoped
キャッシュ実装を使用して、 quarkus.oidc.TokenIntrospection
オブジェクトや quarkus.oidc.UserInfo
オブジェクトを保存および取得します。
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
各 OIDC テナントは、 quarkus.oidc.TokenIntrospection
データ、 quarkus.oidc.UserInfo
データ、またはその両方の保存を許可または拒否できます。これには、ブール値の quarkus.oidc."tenant".allow-token-introspection-cache
プロパティーと quarkus.oidc."tenant".allow-user-info-cache
プロパティーを使用します。
さらに、 quarkus-oidc
は、 quarkus.oidc.TokenIntrospectionCache
と quarkus.oidc.UserInfoCache
の両方のインターフェイスを実装する、シンプルなデフォルトのメモリーベースのトークンキャッシュを提供します。
デフォルトの OIDC トークンキャッシュは、次のように設定すると、有効にできます。
# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value:
quarkus.oidc.token-cache.max-size=1000
# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
quarkus.oidc.token-cache.time-to-live=3M
# 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval':
quarkus.oidc.token-cache.clean-up-timer-interval=1M
デフォルトのキャッシュは、トークンをキーとして使用します。各エントリーには、 TokenIntrospection
、 UserInfo
、またはその両方が含まれます。
最大で max-size
個のエントリーのみが保持されます。
新しいエントリーを追加するときにキャッシュがすでにいっぱいになっている場合は、期限切れのエントリーを 1 つ削除して領域を確保する試みが行われます。
さらに、クリーンアップタイマーを有効にすると、期限切れのエントリーが定期的にチェックされ、削除されます。
デフォルトのキャッシュ実装を試すことも、カスタムのキャッシュ実装を登録することもできます。
JSON Web トークンのクレーム検証
ベアラーの JWT トークンの署名が検証され、その expires at
(exp
) クレームがチェックされると、次に iss
(issuer
) クレーム値が検証されます。
デフォルトでは、 iss
クレーム値は、既知のプロバイダー設定で検出された issuer
プロパティーと比較されます。
ただし、 quarkus.oidc.token.issuer
プロパティーが設定されている場合は、代わりにそのプロパティーと iss
クレーム値が比較されます。
場合によっては、この iss
クレーム検証がうまくいかないことがあります。
たとえば、検出された issuer
プロパティーには内部の HTTP/IP アドレスが含まれているのに、トークン iss
クレーム値には外部の HTTP/IP アドレスが含まれている場合などです。
または、検出された issuer
プロパティーにはテンプレートテナント変数が含まれているのに、トークン iss
クレーム値には完全なテナント固有の発行者の値が含まれている場合などです。
このような場合は、 quarkus.oidc.token.issuer=any
を設定して発行者の検証をスキップすることを検討してください。
発行者の検証のスキップは、他の方法を利用できない場合にのみ行ってください。
-
Keycloak を使用していて、ホストアドレスが異なるために発行者検証エラーが発生する場合は、Keycloak に
KEYCLOAK_FRONTEND_URL
プロパティーを設定して、同じホストアドレスを使用してください。 -
マルチテナントデプロイメントで
iss
プロパティーがテナント固有のものである場合は、SecurityIdentity
tenant-id
属性を使用して、エンドポイントまたはカスタム Jakarta フィルターで発行者が正しいことを確認します。 以下に例を示します。
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.security.identity.SecurityIdentity;
@Provider
public class IssuerValidator implements ContainerRequestFilter {
@Inject
OidcConfigurationMetadata configMetadata;
@Inject JsonWebToken jwt;
@Inject SecurityIdentity identity;
public void filter(ContainerRequestContext requestContext) {
String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id"));
if (!issuer.equals(jwt.getIssuer())) {
requestContext.abortWith(Response.status(401).build());
}
}
}
トークンの |
Jose4j Validator
org.eclipse.microprofile.jwt.JsonWebToken
が初期化される前に、カスタム Jose4j Validator を登録して、JWT クレーム検証プロセスをカスタマイズできます。
以下に例を示します。
package org.acme.security.openid.connect;
import static org.eclipse.microprofile.jwt.Claims.iss;
import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;
@Unremovable
@ApplicationScoped
public class IssuerValidator implements Validator { (1)
@Override
public String validate(JwtContext jwtContext) throws MalformedClaimException {
if (jwtContext.getJwtClaims().hasClaim(iss.name())
&& "my-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) {
return "wrong issuer"; (2)
}
return null; (3)
}
}
1 | すべての OIDC テナントの JWT トークンを検証するために、Jose4j Validator を登録します。 |
2 | クレーム検証エラーの説明を返します。 |
3 | この Validator がトークンを正常に検証したことを確認するために、 null を返します。 |
カスタム Validator を特定の OIDC テナントにのみバインドするには、 @quarkus.oidc.TenantFeature アノテーションを使用します。
|
シングルページアプリケーション
シングルページアプリケーション (SPA) は、通常、 XMLHttpRequest
(XHR) と OIDC プロバイダーが提供する JavaScript ユーティリティーコードを使用してベアラートークンを取得し、Quarkus service
アプリケーションにアクセスします。
たとえば、Keycloak を使用する場合は、 keycloak.js
を使用してユーザーを認証し、SPA から期限切れのトークンを更新できます。
<html>
<head>
<title>keycloak-spa</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script type="importmap">
{
"imports": {
"keycloak-js": "https://cdn.jsdelivr.net/npm/keycloak-js@26.0.7/lib/keycloak.js"
}
}
</script>
<script type="module">
import Keycloak from "keycloak-js";
const keycloak = new Keycloak({
url: 'http://localhost:8180',
realm: 'quarkus',
clientId: 'quarkus-app'
});
await keycloak.init({onLoad: 'login-required'}).then(function () {
console.log('User is now authenticated.');
}).catch(function () {
console.log('User is NOT authenticated.');
});
function makeAjaxRequest() {
axios.get("/api/hello", {
headers: {
'Authorization': 'Bearer ' + keycloak.token
}
})
.then( function (response) {
console.log("Response: ", response.status);
}).catch(function (error) {
console.log('refreshing');
keycloak.updateToken(5).then(function () {
console.log('Token refreshed');
}).catch(function () {
console.log('Failed to refresh token');
window.location.reload();
});
});
}
let button = document.getElementById('ajax-request');
button.addEventListener('click', makeAjaxRequest);
</script>
</head>
<body>
<button id="ajax-request">Request</button>
</body>
</html>
この SPA Keycloak の例で認証を有効にするには、 クライアント認証 を無効にし、 Web origins を次のように設定します。 |
クロスオリジンリソース共有
別のドメインで実行されているシングルページアプリケーションから OIDC service
アプリケーションを使用する予定の場合は、クロスオリジンリソース共有 (CORS) を設定する必要があります。
詳細は、「クロスオリジンリソース共有」ガイドの CORS フィルター セクションを参照してください。
プロバイダーエンドポイント設定
OIDC service
アプリケーションは、OIDC プロバイダーのトークン、 JsonWebKey
(JWK) セット、場合によっては UserInfo
やイントロスペクションのエンドポイントアドレスを確認する必要があります。
デフォルトでは、これらのエンドポイントアドレスは、設定された quarkus.oidc.auth-server-url
に /.well-known/openid-configuration
パスを追加することで検出されます。
検出エンドポイントが利用できない場合や、検出エンドポイントのラウンドトリップのコストを節約する場合は、検出を無効にして、エンドポイントアドレスを相対パス値で設定することができます。 以下に例を示します。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# 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/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
トークンの伝播
ダウンストリームサービスへのベアラーアクセストークンの伝播については、Quarkus の「OpenID Connect (OIDC) および OAuth2 クライアントとフィルターのリファレンス」ガイドの トークンの伝播 セクションを参照してください。
JWT トークン証明書チェーン
場合によっては、JWT ベアラートークンに x5c
ヘッダーがあり、このヘッダーは X509 証明書チェーンを表します。この証明書チェーンのリーフ証明書には、トークンの署名の検証に使用する必要がある公開鍵が含まれています。
まず証明書チェーンを検証してから、この公開鍵を受け入れて署名を検証する必要があります。
証明書チェーンの検証にはいくつかの手順が含まれます。
-
ルート証明書以外のすべての証明書が親証明書によって署名されていることを確認します。
-
チェーンのルート証明書もトラストストアにインポートされていることを確認します。
-
チェーンのリーフ証明書を検証します。リーフ証明書の共通名が設定されている場合は、チェーンのリーフ証明書の共通名がその共通名に一致する必要があります。それ以外の場合は、1 つ以上のカスタム
TokenCertificateValidator
実装が登録されていない限り、チェーンのリーフ証明書もトラストストアで使用可能である必要があります。 -
quarkus.oidc.TokenCertificateValidator
を使用すると、カスタム証明書チェーン検証手順を追加できます。これは、証明書チェーンを持つトークンを要求するすべてのテナント、または@quarkus.oidc.TenantFeature
アノテーションを使用して特定の OIDC テナントにバインドされたすべてのテナントで使用できます。
たとえば、 quarkus.oidc.TokenCertificateValidator
を使用せずにトークンの証明書チェーンを検証するように Quarkus OIDC を設定する方法は次のとおりです。
quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 (1)
quarkus.oidc.certificate-chain.trust-store-password=storepassword
quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com (2)
1 | トラストストアに証明書チェーンのルート証明書が含まれている必要があります。 |
2 | 証明書チェーンのリーフ証明書の共通名は www.quarkusio.com にする必要があります。このプロパティーが設定されていない場合は、1 つ以上のカスタム TokenCertificateValidator 実装が登録されていない限り、トラストストアに証明書チェーンのリーフ証明書が含まれている必要があります。 |
カスタムの quarkus.oidc.TokenCertificateValidator
を登録することで、カスタムの証明書チェーン検証ステップを追加できます。次に例を示します。
package io.quarkus.it.keycloak;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCertificateValidator;
import io.quarkus.oidc.runtime.TrustStoreUtils;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
@Unremovable
public class BearerGlobalTokenChainValidator implements TokenCertificateValidator {
@Override
public void validate(OidcTenantConfig oidcConfig, List<X509Certificate> chain, String tokenClaims) throws CertificateException {
String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1));
JsonObject claims = new JsonObject(tokenClaims);
if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { (1)
throw new CertificateException("Invalid root certificate");
}
}
}
1 | 証明書チェーンのルート証明書がカスタム JWT トークンのクレームにバインドされていることを確認します。 |
OIDC プロバイダーのクライアント認証
quarkus.oidc.runtime.OidcProviderClient
は、OIDC プロバイダーへのリモートリクエストが必要な場合に使用されます。
ベアラートークンのイントロスペクションが必要な場合は、 OidcProviderClient
が OIDC プロバイダーに対して認証する必要があります。
サポートされている認証オプションの詳細は、Quarkus の「Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム」ガイドの OIDC プロバイダーのクライアント認証 セクションを参照してください。
テスト
Keycloak 認証 を必要とする Quarkus OIDC サービスエンドポイントをテストする必要がある場合は、Keycloak 認証のテスト セクションに従ってください。 |
次の依存関係をテストプロジェクトに追加することで、テストを開始できます。
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
Dev Services for Keycloak
The preferred approach for integration testing against Keycloak is Dev Services for Keycloak.
Dev Services for Keycloak
will start and initialize a test container.
Then, it will create a quarkus
realm and a quarkus-app
client with secret
as the client secret. It will also add two users: alice
with both admin
and user
roles, and bob
with the user
role. All of these properties can be customized.
まず、次の依存関係を追加します。この依存関係は、アクセストークンを取得するテストで使用できるユーティリティークラス io.quarkus.test.keycloak.client.KeycloakTestClient
を提供します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-keycloak-server")
次に、 application.properties
設定ファイルを準備します。
空の application.properties
ファイルから開始できます。 Dev Services for Keycloak
が quarkus.oidc.auth-server-url
を登録し、その参照先として実行中のテストコンテナー、 quarkus.oidc.client-id=quarkus-app
、および quarkus.oidc.credentials.secret=secret
を設定するためです。
However, if you have already configured the required quarkus-oidc
properties, then you only need to associate quarkus.oidc.auth-server-url
with the prod
profile for Dev Services for Keycloak
to start a container, as shown in the following example:
%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
最後に、次の例に示すようにテストを記述します。このテストは JVM モードで実行されます。
package org.acme.security.openid.connect;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
@QuarkusTest
public class BearerTokenAuthenticationTest {
KeycloakTestClient keycloakClient = new KeycloakTestClient();
@Test
public void testAdminAccess() {
RestAssured.given().auth().oauth2(getAccessToken("alice"))
.when().get("/api/admin")
.then()
.statusCode(200);
RestAssured.given().auth().oauth2(getAccessToken("bob"))
.when().get("/api/admin")
.then()
.statusCode(403);
}
protected String getAccessToken(String userName) {
return keycloakClient.getAccessToken(userName);
}
}
package org.acme.security.openid.connect;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest {
}
Dev Services for Keycloak の初期化と設定の詳細は、Dev Services for Keycloak ガイドを参照してください。
WireMock
テストプロジェクトに以下の依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
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-service-app
quarkus.oidc.application-type=service
最後にテストコードを記述します。 以下に例を示します。
import static org.hamcrest.Matchers.equalTo;
import java.util.Set;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;
import io.smallrye.jwt.build.Jwt;
@QuarkusTest
@QuarkusTestResource(OidcWiremockTestResource.class)
public class BearerTokenAuthorizationTest {
@Test
public void testBearerToken() {
RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user")))
.when().get("/api/users/me")
.then()
.statusCode(200)
// The test endpoint returns the name extracted from the injected `SecurityIdentity` principal.
.body("userName", equalTo("alice"));
}
private String getAccessToken(String userName, Set<String> groups) {
return Jwt.preferredUserName(userName)
.groups(groups)
.issuer("https://server.example.com")
.audience("https://service.example.com")
.sign();
}
}
quarkus-test-oidc-server
エクステンションは、 JSON Web Key
(JWK
) フォーマットの署名 RSA 秘密鍵ファイルを含んでおり、 smallrye.jwt.sign.key.location
設定プロパティーでそのファイルを参照します。
このエクステンションを使用すると、引数なしの sign()
操作を使用してトークンに署名できます。
OidcWiremockTestResource
を使用して quarkus-oidc
service
アプリケーションをテストすると、通信チャネルも WireMock HTTP スタブに対してテストされるため、最大のカバレッジを得ることができます。
OidcWiremockTestResource
でまだサポートされていない WireMock スタブを使用してテストを実行する必要がある場合は、次の例に示すように、テストクラスに WireMockServer
インスタンスを挿入できます。
|
package io.quarkus.it.keycloak;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWireMock;
import io.restassured.RestAssured;
@QuarkusTest
public class CustomOidcWireMockStubTest {
@OidcWireMock
WireMockServer wireMockServer;
@Test
public void testInvalidBearerToken() {
wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect")
.withRequestBody(matching(".*token=invalid_token.*"))
.willReturn(WireMock.aResponse().withStatus(400)));
RestAssured.given().auth().oauth2("invalid_token").when()
.get("/api/users/me/bearer")
.then()
.statusCode(401)
.header("WWW-Authenticate", equalTo("Bearer"));
}
}
OidcTestClient
SaaS OIDC プロバイダー (Auth0
など) を使用していて、テスト (開発) ドメインに対してテストを実行したり、リモート Keycloak テストレルムに対してテストを実行したりする場合、 quarkus.oidc.auth-server-url
がすでに設定されていれば、 OidcTestClient
を使用できます。
たとえば、次のような設定があるとします。
%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/
%test.quarkus.oidc.client-id=test-auth0-client
%test.quarkus.oidc.credentials.secret=secret
まず、WireMock セクションの説明に従って、同じ依存関係 quarkus-test-oidc-server
を追加します。
次に、以下のようにテストコードを記述します。
package org.acme;
import org.junit.jupiter.api.AfterAll;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import java.util.Map;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;
@QuarkusTest
public class GreetingResourceTest {
static OidcTestClient oidcTestClient = new OidcTestClient();
@AfterAll
public static void close() {
oidcTestClient.close();
}
@Test
public void testHelloEndpoint() {
given()
.auth().oauth2(getAccessToken("alice", "alice"))
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Alice"));
}
private String getAccessToken(String name, String secret) {
return oidcTestClient.getAccessToken(name, secret,
Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/",
"scope", "profile"));
}
}
このテストコードは、クライアント ID が test-auth0-client
のアプリケーションを登録し、パスワードが alice
のユーザー alice
を作成したテスト Auth0
ドメインから password
許可を使用してトークンを取得します。
このようなテストを機能させるには、テスト Auth0
アプリケーションで password
グラントが有効になっている必要があります。
このサンプルコードでは、追加のパラメーターを渡す方法も示しています。 Auth0
の場合、これらは audience
および scope
パラメーターです。
OIDC DevService のテスト
また、 OidcTestClient
を使用して、OIDC 用 Dev Services でサポートされている Quarkus エンドポイントをテストすることもできます。
application.properties
ファイルでの設定は必要ありません。Quarkus が OidcTestClient
を設定します。
package org.acme;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;
@QuarkusTest
public class GreetingResourceTest {
static final OidcTestClient oidcTestClient = new OidcTestClient();
@AfterAll
public static void close() {
oidcTestClient.close();
}
@Test
public void testHelloEndpoint() {
String accessToken = oidcTestClient.getAccessToken("alice", "alice");
given()
.auth().oauth2(accessToken)
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Alice"));
}
}
KeycloakTestResourceLifecycleManager
Keycloak との結合テストには KeycloakTestResourceLifecycleManager
を使用することもできます。
|
まず、以下の依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-keycloak-server")
これは、Keycloak コンテナーを起動する io.quarkus.test.common.QuarkusTestResourceLifecycleManager
の実装である`io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager` を提供します。
Maven Surefire プラグインを次のように設定します。または、ネイティブイメージテストの maven.failsafe.plugin
と同様に設定します。
<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>
REST テストエンドポイントを準備し、次の例に示すように application.properties
を設定します。
# keycloak.url is set by KeycloakTestResourceLifecycleManager
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials=secret
quarkus.oidc.application-type=service
最後にテストコードを記述します。 以下に例を示します。
import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;
@QuarkusTest
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class BearerTokenAuthorizationTest {
@Test
public void testBearerToken() {
RestAssured.given().auth().oauth2(getAccessToken("alice"))
.when().get("/api/users/preferredUserName")
.then()
.statusCode(200)
// The test endpoint returns the name extracted from the injected SecurityIdentity Principal
.body("userName", equalTo("alice"));
}
}
上記の例では、 KeycloakTestResourceLifecycleManager
が alice
と admin
の 2 人のユーザーを登録します。
デフォルトでは、* ユーザー alice
には user
ロールがあります。このロールは、 keycloak.token.user-roles
システムプロパティーを使用してカスタマイズできます。
* ユーザー admin
には user
と admin
の両方のロールがあります。これらのロールは、 keycloak.token.admin-roles
システムプロパティーを使用してカスタマイズできます。
デフォルトでは、 KeycloakTestResourceLifecycleManager
は HTTPS を使用して Keycloak インスタンスを初期化します。これは keycloak.use.https=false
を使用して無効にできます。
デフォルトのレルム名は quarkus
で、クライアント ID は quarkus-service-app
です。
これらの値をカスタマイズする場合は、 keycloak.realm
および keycloak.service.client
システムプロパティーを設定します。
ローカル公開鍵
次の例に示すように、 quarkus-oidc
service
アプリケーションをテストするために、ローカルのインライン公開鍵を使用できます。
quarkus.oidc.client-id=test
quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
smallrye.jwt.sign.key.location=/privateKey.pem
JWT トークンを生成するには、 main
Quarkus リポジトリーの integration-tests/oidc-tenancy
から privateKey.pem
をコピーし、前述の WireMock セクションと同様のテストコードを使用します。
必要に応じて、独自のテストキーを使用することもできます。
この方法では、WireMock 方法と比較してカバレッジが制限されます。 たとえば、リモート通信コードはカバーされません。
TestSecurity アノテーション
@TestSecurity
および @OidcSecurity
アノテーションを使用して、 service
アプリケーションエンドポイントのコードをテストできます。このコードは、次の注入のいずれか 1 つまたは 3 つすべてに依存します。
-
JsonWebToken
-
UserInfo
-
OidcConfigurationMetadata
まず、以下の依存関係を追加します。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-security-oidc")
次の例に示すようにテストコードを記述します。
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.Claim;
import io.quarkus.test.security.oidc.ConfigMetadata;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.UserInfo;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
public void testOidc() {
RestAssured.when().get("test-security-oidc").then()
.body(is("userOidc:viewer"));
}
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(claims = {
@Claim(key = "email", value = "user@gmail.com")
}, userinfo = {
@UserInfo(key = "sub", value = "subject")
}, config = {
@ConfigMetadata(key = "issuer", value = "issuer")
})
public void testOidcWithClaimsUserInfoAndMetadata() {
RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
.body(is("userOidc:viewer:user@gmail.com:subject:issuer"));
}
}
このコード例で使用されている ProtectedResource
クラスは次のようになります。
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/service")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@Inject
UserInfo userInfo;
@Inject
OidcConfigurationMetadata configMetadata;
@GET
@Path("test-security-oidc")
public String testSecurityOidc() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
}
@GET
@Path("test-security-oidc-claims-userinfo-metadata")
public String testSecurityOidcWithClaimsUserInfoMetadata() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
+ ":" + accessToken.getClaim("email")
+ ":" + userInfo.getString("sub")
+ ":" + configMetadata.get("issuer");
}
}
@TestSecurity
アノテーションは常に使用する必要があります。
その user
プロパティーは JsonWebToken.getName()
として返され、その roles
プロパティーは JsonWebToken.getGroups()
として返されます。
@OidcSecurity
アノテーションは任意です。これを使用すると、追加のトークンクレームと UserInfo
および OidcConfigurationMetadata
プロパティーを設定できます。
さらに、 quarkus.oidc.token.issuer
プロパティーが設定されている場合、 OidcConfigurationMetadata
issuer
プロパティー値として使用されます。
不透明トークンを使用する場合は、次のコード例に示す方法でトークンをテストできます。
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.TokenIntrospection;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public void testOidcWithClaimsUserInfoAndMetadata() {
RestAssured.when().get("test-security-oidc-opaque-token").then()
.body(is("userOidc:viewer:userOidc:viewer:user@gmail.com"));
}
}
このコード例で使用されている ProtectedResource
クラスは次のようになります。
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class ProtectedResource {
@Inject
SecurityIdentity securityIdentity;
@Inject
TokenIntrospection introspection;
@GET
@Path("test-security-oidc-opaque-token")
public String testSecurityOidcOpaqueToken() {
return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next()
+ ":" + introspection.getString("username")
+ ":" + introspection.getString("scope")
+ ":" + introspection.getString("email");
}
}
@TestSecurity
、 user
、および roles
属性は、 TokenIntrospection
、 username
、および scope
プロパティーとして使用できます。
io.quarkus.test.security.oidc.TokenIntrospection
を使用して、 email
などの追加のイントロスペクションレスポンスプロパティーを追加します。
これは、複数のテスト方法で同じセキュリティー設定セットを使用する必要がある場合に特に便利です。 |
ログでのエラーの確認
トークン検証エラーの詳細を確認するには、 io.quarkus.oidc.runtime.OidcProvider
および TRACE
レベルのロギングを有効にします。
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.OidcRecorder
および TRACE
レベルのロギングを有効にします。
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
OIDC プロバイダーへの外部および内部アクセス
OIDC プロバイダーおよびその他のエンドポイントの外部からアクセス可能なトークンは、自動検出された URL や quarkus.oidc.auth-server-url
内部 URL を基準に設定された URL とは異なる HTTP(S) URL を持っている可能性があります。
たとえば、SPA が外部トークンエンドポイントアドレスからトークンを取得し、それをベアラートークンとして Quarkus に送信するとします。
その場合、エンドポイントから発行者の検証の失敗が報告される可能性があります。
このような場合に Keycloak を使用する場合は、 KEYCLOAK_FRONTEND_URL
システムプロパティーを外部からアクセス可能なベース URL に設定して Keycloak を起動してください。
他の OIDC プロバイダーを使用する場合は、プロバイダーのドキュメントを参照してください。
`client-id`プロパティーの使用
quarkus.oidc.client-id
プロパティーは、現在のベアラートークンをリクエストした OIDC クライアントを特定します。
OIDC クライアントは、ブラウザーで実行される SPA アプリケーション、または Quarkus service
アプリケーションにアクセストークンを伝播する Quarkus web-app
機密クライアントアプリケーションです。
このプロパティーは、 service
アプリケーションがトークンをリモートでイントロスペクトすることが予想される場合に必要です。これは、不透明トークンの場合は常に当てはまります。
このプロパティーは、ローカル JSON Web Token (JWT) 検証の場合にのみ省略可能です。
エンドポイントがリモートイントロスペクションエンドポイントへのアクセスを必要としない場合でも、 quarkus.oidc.client-id
プロパティーを設定することを推奨します。
これは、 client-id
が設定されている場合、それを使用してトークンオーディエンスを検証できるためです。
また、トークンの検証が失敗した場合にも client-id がログに含まれるため、特定のクライアントに発行されたトークンのトレーサビリティーが向上し、より長い期間にわたる分析が可能になります。
たとえば、OIDC プロバイダーでトークンオーディエンスを設定する場合は、次の設定パターンを検討してください。
# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}
quarkus.oidc.client-id
を設定したが、いずれかの OIDC プロバイダーエンドポイントへのリモートアクセスがエンドポイントに必要ない場合 (イントロスペクション、トークンの取得など)、 quarkus.oidc.credentials
または同様のプロパティーを使用してクライアントシークレットを設定しないでください。その場合、クライアントシークレットは使用されないためです。
Quarkus |
送信者制限アクセストークン
Demonstrating Proof of Possession (DPoP)
RFC9449は 、アクセストークンを現在のクライアントに暗号的にバインドし、アクセストークンの紛失やリプレイを防ぐためのDemonstrating Proof of Possession(DPoP)メカニズムについて記述しています。
シングル・ページ・アプリケーション(SPA)のパブリック・クライアントはDPoPプルーフ・トークンを生成し、DPoPプルーフに暗号学的にバインドされたアクセストークンの取得と提出に使用します。
QuarkusでDPoPサポートを有効にするには、1つのプロパティが必要です。
例:
quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.authorization-scheme=dpop (1)
1 | HTTP Authorization DPoP スキーム値を使用してアクセストークンが提供されることを要求します。 |
カスタムDPoP nonceプロバイダのサポートは将来提供されるかもしれません。
相互 TLS トークンバインディング
RFC8705 では、アクセストークンを Mutual TLS (mTLS) クライアント認証証明書にバインドするメカニズムについて説明しています。
クライアント証明書の SHA256 サムプリントが JWT トークンまたはトークンイントロスペクション確認 x5t#S256
証明書サムプリントと一致する必要があります。
たとえば、 RFC8705 の JWT 証明書のサムプリントの確認方法 および トークンイントロスペクションの確認方法 セクションを参照してください。
MTLS トークンバインディングは キーの所有者
の概念をサポートしており、現在のアクセストークンがこのトークンを提示する現在の認証済みクライアントに発行されたことを確認するために使用できます。
mTLS と OIDC ベアラー認証メカニズムの両方を使用する場合、Quarkus エンドポイントと Quarkus OIDC で mTLS の使用を要求するように設定した後、強制的にアクセストークンが単一のプロパティーで証明書にバインドされるように指定できます。
例:
quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.binding.certificate=true (1)
quarkus.oidc.tls.tls-configuration-name=oidc-client-tls (2)
quarkus.tls.oidc-client-tls.key-store.p12.path=target/certificates/oidc-client-keystore.p12 (2)
quarkus.tls.oidc-client-tls.key-store.p12.password=password
quarkus.tls.oidc-client-tls.trust-store.p12.path=target/certificates/oidc-client-truststore.p12
quarkus.tls.oidc-client-tls.trust-store.p12.password=password
quarkus.http.tls-configuration-name=oidc-server-mtls (3)
quarkus.tls.oidc-server-mtls.key-store.p12.path=target/certificates/oidc-keystore.p12
quarkus.tls.oidc-server-mtls.key-store.p12.password=password
quarkus.tls.oidc-server-mtls.trust-store.p12.path=target/certificates/oidc-server-truststore.p12
quarkus.tls.oidc-server-mtls.trust-store.p12.password=password
1 | ベアラーアクセストークンをクライアント証明書にバインドする必要があることを必須にします。 |
2 | Quarkus OIDC の TLS レジストリー設定により、MTLS 経由で OIDC プロバイダーと通信できるようになります。 |
3 | 外部クライアントが MTLS 経由での Quarkus エンドポイントの認証を必須にする TLS レジストリー設定 |
上記の設定を使用すると、OIDC ベアラートークンをクライアント証明書にバインドすることが必須にできます。
次に、mTLS と OIDC ベアラーセキュリティーアイデンティティーの両方にアクセスする必要がある場合は、 quarkus.http.auth.inclusive=true
を使用して 統合認証 を有効にすることを検討してください。
次のようにして、MTLS と OIDC の両方のセキュリティーアイデンティティーにアクセスできるようになりました。
package io.quarkus.it.oidc;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.security.Authenticated;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class OidcMtlsEndpoint {
@Inject
SecurityIdentity mtlsIdentity; (1)
@Inject
JsonWebToken oidcAccessToken; (2)
@GET
public String getIdentities() {
var cred = identity.getCredential(CertificateCredential.class).getCertificate();
return "Identities: " + cred.getSubjectX500Principal().getName().split(",")[0]
+ ", " + accessToken.getName();
}
}
1 | mTLS が使用され、統合認証が有効になっている場合、 SecurityIdentity は常にプライマリー mTLS 認証を表します。 |
2 | 統合認証を有効にするには、登録されているすべてのメカニズムでセキュリティーアイデンティティーを生成する必要があるため、OIDC セキュリティーアイデンティティーも利用できます。 |
HTTP リクエストが完了した後の認証
場合によっては、アクティブな HTTP リクエストコンテキストが存在しない場合に、特定のトークンの SecurityIdentity
を作成する必要があります。
quarkus-oidc
エクステンションは、トークンを SecurityIdentity
インスタンスに変換するための io.quarkus.oidc.TenantIdentityProvider
を提供します。
たとえば、HTTP リクエストが完了した後にトークンを検証する必要がある状況の 1 つとして、Vert.x イベントバス を使用してメッセージを処理する場合があります。
以下の例では、さまざまな CDI リクエストコンテキスト内で 'product-order' メッセージを使用しています。
したがって、注入された SecurityIdentity
は、検証されたアイデンティティーを正しく表さず、匿名になります。
package org.acme.quickstart.oidc;
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import jakarta.inject.Inject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import io.vertx.core.eventbus.EventBus;
@Path("order")
public class OrderResource {
@Inject
EventBus eventBus;
@POST
public void order(String product, @HeaderParam(AUTHORIZATION) String bearer) {
String rawToken = bearer.substring("Bearer ".length()); (1)
eventBus.publish("product-order", new Product(product, rawToken));
}
public static class Product {
public String product;
public String customerAccessToken;
public Product() {
}
public Product(String product, String customerAccessToken) {
this.product = product;
this.customerAccessToken = customerAccessToken;
}
}
}
1 | この時点では、プロアクティブ認証が無効な場合、トークンは検証されません。 |
package org.acme.quickstart.oidc;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.common.annotation.Blocking;
@ApplicationScoped
public class OrderService {
@Tenant("tenantId")
@Inject
TenantIdentityProvider identityProvider;
@Inject
TenantIdentityProvider defaultIdentityProvider; (1)
@Blocking
@ConsumeEvent("product-order")
void processOrder(OrderResource.Product product) {
AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken);
SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); (2)
...
}
}
1 | デフォルトのテナントの場合、 Tenant 修飾子は任意です。 |
2 | トークンの検証を実行し、トークンを SecurityIdentity に変換します。 |
HTTP リクエスト中にプロバイダーが使用される場合、テナント設定を解決できます。
OpenID Connect マルチテナンシーの使用 ガイドの説明を参照してください。
ただし、アクティブな HTTP リクエストがない場合は、 |
動的テナント設定解決 は現在サポートされていません。 動的テナントを必要とする認証は失敗します。 |
OIDC リクエストフィルター
1 つ以上の OidcRequestFilter
実装を登録すると、Quarkus から OIDC プロバイダーに対して行われた OIDC リクエストをフィルタリングできます。この実装により、新しいリクエストヘッダーを更新または追加したり、リクエストをログに記録したりできます。
詳細は、OIDC リクエストフィルター を参照してください。
OIDC レスポンスフィルター
1 つ以上の OidcResponseFilter
実装を登録することで、OIDC プロバイダーからのレスポンスをフィルタリングできます。これにより、レスポンスのステータス、ヘッダー、本文をチェックして、ログに記録したり、その他のアクションを実行したりできます。
すべての OIDC レスポンスをインターセプトする単一のフィルターを使用することも、 @OidcEndpoint
アノテーションを使用してこのフィルターを特定のエンドポイントレスポンスにのみ適用することもできます。例:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.logging.Log;
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.DISCOVERY) (1)
public class DiscoveryEndpointResponseFilter implements OidcResponseFilter {
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); (2)
if (contentType.equals("application/json") {
String tenantId = rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE); (3)
String metadata = rc.responseBody().toString(); (4)
Log.debugf("Tenant %s OIDC metadata: %s", tenantId, metadata);
}
}
}
1 | このフィルターを、OIDC 検出エンドポイントのみを対象とするリクエストに制限します。 |
2 | レスポンスの Content-Type ヘッダーを確認します。 |
3 | テナント ID を取得するには、 OidcRequestContextProperties リクエストプロパティーを使用します。 |
4 | レスポンスデータを文字列として取得します。 |
Restricting OIDC request and response filters to bearer token flow
When you have both the bearer access token and authorization code flows supported by multiple OIDC tenants and the filters have to deal with a flow specific logic, you can instead have them restricted to the bearer token flow with the io.quarkus.oidc.BearerTokenAuthentication
annotation and the authorization code flow with the 'io.quarkus.oidc.AuthorizationCodeFlow' annotation.
例:
package io.quarkus.it.keycloak;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.BearerTokenAuthentication;
import io.quarkus.oidc.common.OidcRequestFilter;
import jakarta.enterprise.context.ApplicationScoped;
@BearerTokenAuthentication
@ApplicationScoped
@Unremovable
public class CustomOidcRequestFilter implements OidcRequestFilter {
@Override
public void filter(OidcRequestContext requestContext) {
requestContext.request().putHeader("custom-header-name", "custom-header-value");
}
}
プログラムによる OIDC のスタートアップ
OIDC テナントは、次の例のようにプログラムで作成できます。
package io.quarkus.it.oidc;
import io.quarkus.oidc.Oidc;
import jakarta.enterprise.event.Observes;
public class OidcStartup {
void observe(@Observes Oidc oidc) {
oidc.createServiceApp("http://localhost:8180/realms/quarkus");
}
}
上記のコードは、 application.properties
ファイル内の次の設定とプログラム的に同等です。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
さらに多くの OIDC テナントプロパティーを設定する必要がある場合は、次の例のように OidcTenantConfig
ビルダーを使用します。
package io.quarkus.it.oidc;
import io.quarkus.oidc.Oidc;
import io.quarkus.oidc.OidcTenantConfig;
import jakarta.enterprise.event.Observes;
public class OidcStartup {
void createDefaultTenant(@Observes Oidc oidc) {
var defaultTenant = OidcTenantConfig
.authServerUrl("http://localhost:8180/realms/quarkus")
.token().requireJwtIntrospectionOnly().end()
.build();
oidc.create(defaultTenant);
}
}
複数のテナントが関係するより複雑な設定については、OpenID Connect マルチテナンシーガイドの マルチテナントアプリケーション用のプログラムによる OIDC の起動 セクションを参照してください。
Step Up Authentication
The io.quarkus.oidc.AuthenticationContext
annotation can be used to list one or more Authentication Context Class Reference (ACR) values to enforce a required authentication level for the Jakarta REST resource classes and methods.
The OAuth 2.0 Step Up Authentication Challenge Protocol introduces a mechanism for resource servers to request stronger authentication methods when the token does not have expected Authentication Context Class Reference (ACR) values.
Consider the following example:
package io.quarkus.it.oidc;
import io.quarkus.oidc.AuthenticationContext;
import io.quarkus.oidc.BearerTokenAuthentication;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@BearerTokenAuthentication
@Path("/")
public class GreetingsResource {
@Path("hello")
@AuthenticationContext("myACR") (1)
@GET
public String hello() {
return "hello";
}
@Path("hi")
@AuthenticationContext(value = "myACR", maxAge = "PT120m") (2)
@GET
public String hi() {
return "hi";
}
}
1 | Bearer access token must have an acr claim with the myACR ACR value. |
2 | Bearer access token must have an acr claim with the myACR ACR value and be in use for no longer than 120 minutes since the authentication time. |
quarkus.http.auth.proactive=false (1)
1 | Disable proactive authentication so that the @AuthenticationContext annotation can be matched with the endpoint before Quarkus authenticates incoming requests. |
If the bearer access token claim acr
does not contain myACR
, Quarkus returns an authentication requirements challenge indicating required acr_values
:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="insufficient_user_authentication", error_description="A different authentication level is required", acr_values="myACR"
When a client such as Single-page application (SPA) receives a challenge with the insufficient_user_authentication
error code, it must parse acr_values
, request a new user login which must meet the acr_values
constraints, and use a new access token to access Quarkus.
The |
It is also possible to enforce the required authentication level for an OIDC tenant:
quarkus.oidc.hr.token.required-claims.acr=myACR
Or, if you need more flexibility, write a Jose4j Validator:
package io.quarkus.it.oidc;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationFailedException;
@Unremovable
@ApplicationScoped
@TenantFeature("hr")
public class AcrValueValidator implements Validator {
@Override
public String validate(JwtContext jwtContext) throws MalformedClaimException {
var jwtClaims = jwtContext.getJwtClaims();
if (jwtClaims.hasClaim("acr")) {
var acrClaim = jwtClaims.getStringListClaimValue("acr");
if (acrClaim.contains("myACR") && acrClaim.contains("yourACR")) {
return null;
}
}
String requiredAcrValues = "myACR,yourACR";
throw new AuthenticationFailedException(Map.of(OidcConstants.ACR_VALUES, requiredAcrValues));
}
}