Quarkus OpenID Connect (OIDC) エクステンションを使用して、ベアラートークン認証により、アプリケーション内の Jakarta REST (旧称 JAX-RS) エンドポイントへの HTTP アクセスを保護します。
Quarkus は、Quarkus OpenID Connect (OIDC) エクステンションを通じて、ベアラートークン認証メカニズムをサポートします。
ベアラートークンは、OIDC と、Keycloak などの OAuth 2.0 準拠の認可サーバーによって発行されます。
ベアラートークン認証は、ベアラートークンの存在と有効性に基づいて HTTP リクエストを承認するプロセスです。ベアラートークンは、呼び出しのサブジェクトに関する情報を提供します。これは、HTTP リソースにアクセスできるかどうかを判断するために使用されます。
次の図は、Quarkus のベアラートークン認証メカニズムの概要を示しています。
Quarkus サービスは、OIDC プロバイダーから検証キーを取得します。検証キーは、ベアラーアクセストークンの署名を検証するために使用されます。
Quarkus ユーザーは、シングルページアプリケーション (SPA) にアクセスします。
単一ページアプリケーションは、Authorization Code Flow を使用してユーザーを認証し、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 トークンクレームにアクセスする必要がある場合は、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";
}
}
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";
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
JsonWebToken
の注入は、@ApplicationScoped
、@Singleton
、および @RequestScoped
スコープでサポートされています。ただし、個々のクレームが単純な型として注入される場合は、@RequestScoped
を使用する必要があります。詳細は、Quarkus の「JWT RBAC の使用」ガイドの サポートされている注入スコープ セクションを参照してください。
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 エンドポイントをサポートしている場合だけです。
検証済みの 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"
],
}
}
{
"iss": "https://server.example.com",
"sub": "24400320",
"upn": "jdoe@example.com",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"groups": {
"roles": [
"microprofile_jwt_user"
],
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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 tips and tricks」ガイドの Security identity customization セクションを参照してください。
HTTP セキュリティーポリシー を使用して、トークンクレームから作成された SecurityIdentity
ロールをデプロイメント固有のロールにマップすることもできます。
SecurityIdentity
権限は、ロールのソース のスコープパラメーターから 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")
@GET
@Path("/email")
public Boolean isUserEmailAddressVerifiedByUser() {
return accessToken.getClaim(Claims.email_verified.name());
}
@PermissionsAllowed("orders_read")
@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;
}
}
}
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;
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
JWT トークンのイントロスペクションをリモートで間接的に実施することには、利点と欠点があります。利点は、2 つのリモート呼び出し (リモート OIDC メタデータ検出呼び出しと、それに続く使用されない検証キーを取得するための別のリモート呼び出し) が不要になることです。欠点は、イントロスペクションエンドポイントアドレスを知って、手動で設定する必要があることです。
この他にも、OIDC メタデータ検出のデフォルトオプションを許可しながら、リモート JWT イントロスペクションのみの実行を要求する方法があります (次の例を参照)。
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
このアプローチの利点は、設定がよりシンプルで理解しやすいことです。欠点は、検証キーが取得されない場合でも、イントロスペクションエンドポイントアドレスを検出するためにリモート OIDC メタデータ検出呼び出しが必要になることです。
シンプルな jakarta.json.JsonObject
ラッパーオブジェクトである io.quarkus.oidc.TokenIntrospection
が作成されます。JWT または不透明トークンのいずれかが正常にイントロスペクトされている場合、SecurityIdentity
introspection
属性として注入またはアクセスできます。
すべての不透明アクセストークンは、リモートでイントロスペクトする必要があります。場合によっては、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 {
...
}
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
各 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:
'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval':
# '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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
デフォルトのキャッシュはトークンをキーとして使用し、各エントリーには TokenIntrospection
、UserInfo
、またはその両方を含めることができます。max-size
のエントリー数までのみが保持されます。新しいエントリーを追加するときにキャッシュがすでにいっぱいになっている場合は、期限切れのエントリーを 1 つ削除してスペースを見つけようとします。さらに、クリーンアップタイマーをアクティベートにすると、期限切れのエントリーが定期的にチェックされ、削除されます。
デフォルトのキャッシュ実装を試したり、カスタムのキャッシュ実装を登録したりできます。
ベアラー 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_FRONTEND_URL
プロパティーを使用して Keycloak を設定し、同じホストアドレスが使用されるようにします。
マルチテナントデプロイメントで 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());
}
}
}
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());
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
quarkus.oidc.token.audience
プロパティーを使用して、トークン aud
(audience
) クレーム値を検証することを検討してください。
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 {
@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";
}
return null;
}
}
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
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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>
<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>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
この SPA Keycloak の例の認証を有効にするには、Client authentication を無効にし、Web origins を http://localhost:8080
に設定します。これらの設定により、Keycloak の CORS ポリシーは Quarkus アプリケーションと通信できるようになります。このコードは、Keycloak と統合された Quarkus シングルページアプリケーションの構築例を示しています。Keycloak を統合したシングルページアプリケーションの作成の詳細は、公式の Keycloak JavaScript adapter ドキュメント を参照してください。
別のドメインで実行されているシングルページアプリケーションから OIDC サービス
アプリケーションを使用する予定の場合は、Cross-Origin Resource Sharing (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.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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
ダウンストリームサービスへのベアラーアクセストークンの伝播に関する詳細は、Quarkus の「OpenID Connect (OIDC) と OAuth2 クライアントおよびフィルターのリファレンス」ガイドの トークンの伝播 セクションを参照してください。
場合によっては、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
quarkus.oidc.certificate-chain.trust-store-password=storepassword
quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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"))) {
throw new CertificateException("Invalid root certificate");
}
}
}
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");
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
証明書チェーンのルート証明書がカスタム JWT トークンのクレームにバインドされていることを確認します。
quarkus.oidc.runtime.OidcProviderClient
は、OIDC プロバイダーへのリモートリクエストが必要な場合に使用されます。ベアラートークンのイントロスペクションが必要な場合は、OidcProviderClient
が OIDC プロバイダーに対して認証する必要があります。サポートされている認証オプションの詳細は、Quarkus の「Web アプリケーションを保護するための OpenID Connect 認可コードフローメカニズム」ガイドの OIDC プロバイダークライアント認証 セクションを参照してください。
次の依存関係をテストプロジェクトに追加することで、テストを開始できます。
Maven を使用する場合:
<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>
<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>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Gradle を使用する場合:
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Keycloak に対する結合テストを行う際に推奨される手法は、Dev Services for Keycloak です。Dev Services for Keycloak
が起動し、テストコンテナーを初期化します。次に、quarkus
レルムと quarkus-app
クライアント (secret
シークレット) を作成し、alice
(admin
および user
ロール) および bob
(user
ロール) ユーザーを追加します。これらのプロパティーはすべてカスタマイズできます。
まず、アクセストークンを取得するためのテストで使用できるユーティリティークラス io.quarkus.test.keycloak.client.KeycloakTestClient
を提供する次の依存関係を追加します。
Maven を使用する場合:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-keycloak-server")
testImplementation("io.quarkus:quarkus-test-keycloak-server")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
次に、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
を `Dev Services for Keycloak` の prod
プロファイルに関連付けるだけでコンテナーを起動できます。
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
テストを実行する前にカスタムレルムファイルを Keycloak にインポートする必要がある場合は、次のように Dev Services for Keycloak
を設定します。
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
最後に、次の例に示すように、JVM モードで実行されるテストを記述します。
Dev Services for Keycloak の初期化と設定の詳細は、Dev Services for Keycloak ガイドを参照してください。
テストプロジェクトに次の依存関係を追加します。
Maven を使用する場合:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-oidc-server")
testImplementation("io.quarkus:quarkus-test-oidc-server")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
REST テストエンドポイントを準備し、application.properties
を設定します。以下に例を示します。
keycloak.url is set by OidcWiremockTestResource
# 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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
最後にテストコードを記述します。以下に例を示します。
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();
}
}
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();
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
quarkus-test-oidc-server
エクステンションには、JSON Web Key
(JWK
) 形式の署名 RSA 秘密鍵ファイルが含まれており、smallrye.jwt.sign.key.location
設定プロパティーでそのファイルを指します。引数なしの sign()
操作を使用してトークンに署名できます。
OidcWiremockTestResource
を使用して quarkus-oidc
service
アプリケーションをテストすると、通信チャネルも WireMock HTTP スタブに対してテストされるため、最高のカバレッジが提供されます。OidcWiremockTestResource
でまだサポートされていない WireMock スタブを使用してテストを実行する必要がある場合は、次の例に示すように、テストクラスに WireMockServer
インスタンスを注入できます。
OidcWiremockTestResource
は、Docker コンテナーに対して @QuarkusIntegrationTest
では機能しません。これは、WireMock サーバーがテストを実行する JVM で実行され、Quarkus アプリケーションを実行する Docker コンテナーからはアクセスできないためです。
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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Auth0
などの SaaS OIDC プロバイダーを使用していて、テスト (開発) ドメインに対してテストを実行したり、リモート 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
%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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
まず、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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
このテストコードは、クライアント ID test-auth0-client
でアプリケーションを登録し、パスワード alice
でユーザー alice
を作成したテスト Auth0
ドメインから password
付与を使用してトークンを取得します。このようなテストが機能するには、テスト Auth0
アプリケーションで password
付与が有効になっている必要があります。このサンプルコードでは、追加のパラメーターを渡す方法も示しています。Auth0
の場合、これらは audience
と scope
パラメーターです。
OidcTestClient
を使用して、Dev Services for OIDC でサポートされている 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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
次の例に示すように、ローカルのインライン公開鍵を使用して 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
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
JWT トークンを生成するには、main
Quarkus リポジトリーの integration-tests/oidc-tenancy
から privateKey.pem
をコピーし、前の WireMock セクションのものと同様のテストコードを使用します。必要に応じて、独自のテストキーを使用することもできます。
このアプローチでは、WireMock アプローチと比較してカバレッジが制限されます。たとえば、リモート通信コードはカバーされません。
@TestSecurity
および @OidcSecurity
アノテーションを使用して、次のインジェクションのいずれか 1 つまたは 3 つすべてに依存する service
アプリケーションエンドポイントコードをテストできます。
JsonWebToken
UserInfo
OidcConfigurationMetadata
まず、次の依存関係を追加します。
Maven を使用する場合:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
Gradle を使用する場合:
testImplementation("io.quarkus:quarkus-test-security-oidc")
testImplementation("io.quarkus:quarkus-test-security-oidc")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
次の例に示すようにテストコードを記述します。
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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
このコード例で使用されている 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");
}
}
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");
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
常に @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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
このコード例で使用されている 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");
}
}
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");
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
@TestSecurity
、user
、および roles
属性は、TokenIntrospection
、username
、および scope
プロパティーとして使用できます。io.quarkus.test.security.oidc.TokenIntrospection
を使用して、email
などの追加のイントロスペクション応答プロパティーを追加します。
@TestSecurity
と @OidcSecurity
は、次の例に示すように、メタアノテーションで組み合わせることができます。
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public @interface TestSecurityMetaAnnotation {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public @interface TestSecurityMetaAnnotation {
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
これは、複数のテスト方法で同じセキュリティー設定セットを使用する必要がある場合に特に便利です。
トークン検証エラーの詳細を確認するには、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
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
OIDC プロバイダーおよびその他のエンドポイントの外部アクセス可能なトークンは、quarkus.oidc.auth-server-url
内部 URL を基準として自動検出された URL または設定された URL とは異なる HTTP(S) URL を持つ場合があります。たとえば、SPA が外部トークンエンドポイントアドレスからトークンを取得し、それをベアラートークンとして Quarkus に送信するとします。その場合、エンドポイントは発行者の検証の失敗を報告する可能性があります。
このような場合、Keycloak を使用する場合は、KEYCLOAK_FRONTEND_URL
システムプロパティーを外部からアクセス可能なベース URL に設定して起動します。他の OIDC プロバイダーを使用する場合は、プロバイダーのドキュメントを参照してください。
quarkus.oidc.client-id
プロパティーは、現在のベアラートークンをリクエストした OIDC クライアントを識別します。OIDC クライアントは、ブラウザーで実行される SPA アプリケーション、または Quarkus service
アプリケーションにアクセストークンを伝播する Quarkus web-app
機密クライアントアプリケーションです。
service
アプリケーションがトークンをリモートでイントロスペクトすることが予想される場合、このプロパティーは必須です。これは、不透明トークンの場合は常に該当します。このプロパティーは、ローカル JSON Web Token (JWT) 検証の場合のみのオプションです。
エンドポイントがリモートイントロスペクションエンドポイントへのアクセスを必要としない場合でも、quarkus.oidc.client-id
プロパティーを設定することを推奨します。これは、client-id
が設定されている場合、それを使用してトークン audience を検証できるためです。また、トークンの検証が失敗した場合にもログに含まれるため、特定のクライアントに発行されたトークンの追跡可能性が向上し、より長い期間にわたる分析が可能になります。
たとえば、OIDC プロバイダーがトークン audience を設定する場合は、次の設定パターンを検討してください。
Set client-id
Token audience claim must contain 'quarkus-app'
# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
quarkus.oidc.client-id
を設定したが、エンドポイントが OIDC プロバイダーエンドポイントの 1 つへのリモートアクセスを必要としない場合 (イントロスペクション、トークンの取得など) は、quarkus.oidc.credentials
または同様のプロパティーを使用してクライアントシークレットを設定しないでください (このプロパティーは使用されないため)。
Quarkus web-app
アプリケーションには常に quarkus.oidc.client-id
プロパティーが必要です。