OpenID Connect (OIDC) ベアラートークン認証
Quarkus OpenID Connect (OIDC) エクステンションを使用することで、ベアラートークン認証を使って、アプリケーション内の Jakarta REST (旧称 JAX-RS) エンドポイントへの HTTP アクセスを保護できます。
Quarkus のベアラートークン認証メカニズムの概要
Quarkus は、Quarkus OpenID Connect (OIDC) エクステンションを通じて、ベアラートークン認証メカニズムをサポートしています。
ベアラートークンは、OIDC および OAuth 2.0 準拠の認可サーバー (Keycloak など) によって発行されます。
ベアラートークン認証は、ベアラートークンの存在と有効性に基づいて HTTP リクエストを認可するプロセスです。 ベアラートークンは、呼び出しの主体に関する情報を提供します。この情報は、HTTP リソースにアクセスできるかどうかを判断するために使用されます。
以下の図に、Quarkus のベアラートークン認証メカニズムの概要を示します。
-
Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
-
Quarkus ユーザーは、シングルページアプリケーション (SPA) にアクセスします。
-
シングルページアプリケーションは、認可コードフローを使用してユーザーを認証し、OIDC プロバイダーからトークンを取得します。
-
シングルページアプリケーションは、アクセストークンを使用して、Quarkus サービスからサービスデータを取得します。
-
Quarkus サービスは、検証キーを使用してベアラーアクセストークンの署名を検証し、トークンの有効期限やその他のクレームをチェックします。トークンが有効であればリクエストの続行を許可し、サービスレスポンスをシングルページアプリケーションに返します。
-
シングルページアプリケーションは、Quarkus ユーザーに同じデータを返します。
-
Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。 検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
-
クライアントは、クライアント ID とシークレットを必要とする
client_credentials、またはクライアント ID、シークレット、ユーザー名、およびパスワードを必要とするパスワードグラントを使用して、OIDC プロバイダーからアクセストークンを取得します。 -
クライアントは、アクセストークンを使用して、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 は、次のいずれかの条件が満たされた場合に自動的に有効になります。
-
if
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 ロール
検証済みの JWT アクセストークンから、次のように SecurityIdentity ロールをマッピングできます。
-
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 の「セキュリティーに関するヒントとコツ」ガイドの Security identity customization セクションを参照してください。
また、HTTP Security policy を使用して、トークンクレームから作成された SecurityIdentity ロールをデプロイメント固有のロールにマップすることもできます。
トークンスコープと SecurityIdentity パーミッション
SecurityIdentity パーミッションは、source of the roles のスコープパラメーターから io.quarkus.security.StringPermission の形式で、同じクレームセパレーターを使用してマッピングされます。
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 エンドポイントの認可」ガイドの Permission annotation セクションを参照してください。
トークンの検証とイントロスペクション
トークンが 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 トークンのリモートでのイントロスペクションを間接的に強制することには、利点と欠点があります。 利点は、リモート OIDC メタデータ検出呼び出しと、それに続く使用されない検証キーを取得するための別のリモート呼び出しという、2 つのリモート呼び出しが不要になることです。 欠点は、イントロスペクションエンドポイントアドレスを知っていて、手動で設定する必要があることです。
代替のアプローチは、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 は、 @ApplicationScoped キャッシュ実装に使用できる quarkus.oidc.TokenIntrospectionCache および quarkus.oidc.UserInfoCache インターフェースを提供します。
次の例に示すように、 @ApplicationScoped キャッシュ実装を使用して、 quarkus.oidc.TokenIntrospection および/または quarkus.oidc.UserInfo オブジェクトを保存および取得します。
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
各 OIDC テナントは、ブール値の quarkus.oidc."tenant".allow-token-introspection-cache および quarkus.oidc."tenant".allow-user-info-cache プロパティーを使用して、その quarkus.oidc.TokenIntrospection データ、 quarkus.oidc.UserInfo データ、またはその両方の保存を許可または拒否できます。
さらに、 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プロパティーがテナント固有のものである場合は、SecurityIdentitytenant-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-junit</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit")
Dev Services for Keycloak
Keycloak に対する結合テストには、Dev Services for Keycloak を使用することを推奨します。 Dev Services for Keycloak は、テストコンテナーを起動して初期化します。次に、 quarkus レルムと、クライアントシークレットが secret の quarkus-app クライアントを作成します。また、admin と user の両方のロールを持つ alice と、 user ロールを持つ bob の 2 人のユーザーも追加します。これらのプロパティーはすべてカスタマイズできます。
まず、次の依存関係を追加します。この依存関係は、アクセストークンを取得するテストで使用できるユーティリティークラス 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 設定ファイルを準備します。 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-url を prod プロファイルに関連付けるだけで、Dev Services for Keycloak がコンテナーを起動できます。
%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 秘密鍵ファイルを含んでおり、 smallryy.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 プロバイダー は、io.quarkus.oidc.DPoPNonceProvider インターフェースを CDI Bean として実装することで定義できます。
DPoP プルーフ nonce が不足しているか無効な場合、Quarkus は 401 Unauthorized ステータスで応答し、期待される nonce を DPoP-Nonce ヘッダー値として添付します。
Mutual 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 リクエストフィルター
Quarkus が OIDC プロバイダーに対して行う OIDC リクエストは、1 つ以上の OidcRequestFilter 実装を登録することでフィルタリングできます。これにより、新しいリクエストヘッダーを更新または追加したり、リクエストをログに記録したりできます。詳細は、OIDC リクエストフィルター を参照してください。
OIDC レスポンスフィルター
OIDC プロバイダーからのレスポンスは、1 つ以上の OidcResponseFilter 実装を登録することでフィルタリングできます。これにより、レスポンスのステータス、ヘッダー、本文をチェックして、ログに記録したり、その他のアクションを実行したりできます。
すべての 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.smallrye.mutiny.Uni;
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 Uni<Void> filter(OidcResponseFilterContext 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);
}
return Uni.createFrom().voidItem();
}
}
| 1 | このフィルターを、OIDC ディスカバリーエンドポイントのみを対象とするリクエストに制限します。 |
| 2 | レスポンスの Content-Type ヘッダーを確認します。 |
| 3 | テナント ID を取得するには、 OidcRequestContextProperties リクエストプロパティーを使用します。 |
| 4 | レスポンスデータを文字列として取得します。 |
OIDC リクエストおよびレスポンスフィルターをベアラートークンフローに制限する
「ベアラーアクセストークン」と 認可コード の両フローが 複数の OIDC テナント でサポートされており、フィルターがフロー固有のロジックを処理する必要がある場合、代わりに io.quarkus.oidc.BearerTokenAuthentication アノテーションでベアラートークンフローに制限するか、「io.quarkus.oidc.AuthorizationCodeFlow」アノテーションで認可コードフローに制限する ことができます。
例えば:
package io.quarkus.it.keycloak;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.BearerTokenAuthentication;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
@BearerTokenAuthentication
@ApplicationScoped
@Unremovable
public class CustomOidcRequestFilter implements OidcRequestFilter {
@Override
public Uni<Void> filter(OidcRequestFilterContext requestContext) {
requestContext.request().putHeader("custom-header-name", "custom-header-value");
return Uni.createFrom().voidItem();
}
}
プログラムによる 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 起動 セクションを参照してください。
ステップアップ認証
io.quarkus.oidc.AuthenticationContext アノテーション は、Jakarta REST リソースクラスとメソッドに必要な認証レベルを適用するために、1 つ以上の Authentication Context Class Reference (ACR) 値をリストするために使用できます。
OAuth 2.0 Step Up Authentication Challenge Protocol は、トークンに予期される Authentication Context Class Reference (ACR) 値がない場合に、リソースサーバーがより強力な認証メソッドを要求するためのメカニズムを導入しています。
次の例を考えます。
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 | ベアラーアクセストークンは、 myACR ACR 値を持つ acr クレームを持っている必要があります。 |
| 2 | ベアラーアクセストークンは、 myACR ACR 値を持つ acr クレームを持ち、認証時刻から 120 分を超えて使用されていない必要があります。 |
quarkus.http.auth.proactive=false (1)
| 1 | Quarkus が受信リクエストを認証する前に、 @AuthenticationContext アノテーションをエンドポイントに一致させることができるように、プロアクティブ認証を無効にします。 |
ベアラーアクセストークンのクレーム acr が myACR を含まない場合、Quarkus は必要な 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"
シングルページアプリケーション (SPA) などのクライアントが insufficient_user_authentication エラーコードを含むチャレンジを受け取った場合、 acr_values を解析し、 acr_values の制約を満たす新しいユーザーログインを要求し、新しいアクセストークンを使用して Quarkus にアクセスする必要があります。
|
|
OIDC テナントに必要な認証レベルを適用することも可能です:
quarkus.oidc.hr.token.required-claims.acr=myACR
あるいは、より柔軟性が必要な場合は、Jose4j Validator を作成してください:
package io.quarkus.it.oidc;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import org.eclipse.microprofile.jwt.Claims;
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();
var acrClaimName = Claims.acr.name();
if (jwtClaims.hasClaim(acrClaimName)) {
// The claim 'acr' could be a String or a list
List<String> acrClaimValues;
if (jwtClaims.isClaimValueStringList(acrClaimName)) {
acrClaimValues = jwtClaims.getStringListClaimValue(acrClaimName);
} else if (jwtClaims.isClaimValueString(acrClaimName)) {
acrClaimValues = List.of(jwtClaims.getStringClaimValue(acrClaimName));
} else {
throw new MalformedClaimException("Claim '" + acrClaimName + "' is not a String or List of Strings.");
}
if (acrClaimValues.contains("myACR") && acrClaimValues.contains("yourACR")) {
return null;
}
}
String requiredAcrValues = "myACR,yourACR";
throw new AuthenticationFailedException(Map.of(OidcConstants.ACR_VALUES, requiredAcrValues));
}
}