1
0
mirror of https://github.com/keycloak/keycloak.git synced 2026-10-23 19:29:23 +03:00

[OID4VCI] Conformance Non Haip configuration and execution. (#52271)

Closes #51389


Signed-off-by: Lukas Hanusovsky <lhanusov@redhat.com>
This commit is contained in:
Lukas Hanusovsky
2026-09-09 08:27:29 +02:00
committed by GitHub
parent 7dac019b04
commit bdfdba236f
94 changed files with 2078 additions and 448 deletions

View File

@@ -33,6 +33,10 @@ jobs:
needs:
- build
timeout-minutes: 45
strategy:
matrix:
suite: [Oid4VpTestSuite, Oid4VciHaipTestSuite, Oid4VciNonHaipTestSuite]
fail-fast: false
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@@ -40,8 +44,5 @@ jobs:
name: Integration test setup
uses: ./.github/actions/integration-test-setup
- name: Build tar keycloak-quarkus-dist
run: ./mvnw package -pl quarkus/server/,quarkus/dist/
- name: Run conformance tests
run: ./mvnw package -f tests/conformance/pom.xml
run: ./mvnw package -f tests/conformance/pom.xml -Dtest=${{ matrix.suite }}

View File

@@ -26,6 +26,6 @@ public class ConformanceTestFrameworkExtension implements TestFrameworkExtension
@Override
public List<Supplier<?, ?>> suppliers() {
return List.of(new OpenIdConformanceSuiteSupplier());
return List.of(new OpenIdConformanceServerSupplier());
}
}

View File

@@ -37,9 +37,9 @@ import org.testcontainers.containers.Network;
import org.testcontainers.containers.wait.strategy.Wait;
import org.testcontainers.utility.DockerImageName;
public final class OpenIdConformanceSuite implements AutoCloseable {
public final class OpenIdConformanceServer implements AutoCloseable {
private static final Logger LOGGER = Logger.getLogger(OpenIdConformanceSuite.class);
private static final Logger LOGGER = Logger.getLogger(OpenIdConformanceServer.class);
public static final String MONGODB_CONTAINER = "mongodb";
public static final String CONFORMANCE_CONTAINER = "conformance";
@@ -53,7 +53,7 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
private static final String NGINX_CERTIFICATE_PATH = "/etc/ssl/certs/nginx-selfsigned.crt";
private static OpenIdConformanceSuite instance;
private static OpenIdConformanceServer instance;
private final Network network;
private final GenericContainer<?> mongo;
@@ -63,8 +63,8 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
private final URI baseUri;
private final X509Certificate nginxCertificate;
private OpenIdConformanceSuite(Network network, GenericContainer<?> mongo, GenericContainer<?> server,
GenericContainer<?> nginx, URI baseUri, SSLContext sslContext, X509Certificate nginxCertificate) {
private OpenIdConformanceServer(Network network, GenericContainer<?> mongo, GenericContainer<?> server,
GenericContainer<?> nginx, URI baseUri, SSLContext sslContext, X509Certificate nginxCertificate) {
this.network = network;
this.mongo = mongo;
this.server = server;
@@ -78,7 +78,7 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
* The suite is a singleton as it is also used to discover module variants while tests are collected, before
* the test framework injects it.
*/
public static synchronized OpenIdConformanceSuite instance() {
public static synchronized OpenIdConformanceServer instance() {
if (instance == null) {
// Must be exposed before the containers start so they can reach Keycloak on the Docker host
Testcontainers.exposeHostPorts(KEYCLOAK_BASE_URI.getPort());
@@ -87,7 +87,7 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
return instance;
}
private static OpenIdConformanceSuite start() {
private static OpenIdConformanceServer start() {
LOGGER.trace("Starting Conformance Suite with MongoDB (storage) and Nginx (access point)");
Network network = Network.newNetwork();
@@ -131,8 +131,8 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
URI baseUri = URI.create("https://" + nginx.getHost() + ":" + nginx.getMappedPort(8443));
X509Certificate nginxCertificate = nginxCertificate(nginx);
SSLContext sslContext = sslContextTrusting(nginxCertificate);
OpenIdConformanceSuite suite =
new OpenIdConformanceSuite(network, mongo, server, nginx, baseUri, sslContext, nginxCertificate);
OpenIdConformanceServer suite =
new OpenIdConformanceServer(network, mongo, server, nginx, baseUri, sslContext, nginxCertificate);
suite.client().waitUntilAvailable(Duration.ofMinutes(4));
return suite;
} catch (RuntimeException e) {
@@ -164,7 +164,7 @@ public final class OpenIdConformanceSuite implements AutoCloseable {
public void close() {
List.of(nginx, server, mongo).forEach(GenericContainer::stop);
network.close();
synchronized (OpenIdConformanceSuite.class) {
synchronized (OpenIdConformanceServer.class) {
if (instance == this) {
instance = null;
}

View File

@@ -17,18 +17,18 @@
package org.keycloak.testframework.conformance;
import org.keycloak.testframework.conformance.annotations.InjectConformanceSuite;
import org.keycloak.testframework.conformance.annotations.InjectOpenIdConformanceServer;
import org.keycloak.testframework.injection.InstanceContext;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.injection.RequestedInstance;
import org.keycloak.testframework.injection.Supplier;
public class OpenIdConformanceSuiteSupplier implements Supplier<OpenIdConformanceSuite, InjectConformanceSuite> {
public class OpenIdConformanceServerSupplier implements Supplier<OpenIdConformanceServer, InjectOpenIdConformanceServer> {
@Override
public OpenIdConformanceSuite getValue(InstanceContext<OpenIdConformanceSuite, InjectConformanceSuite> instanceContext) {
return OpenIdConformanceSuite.instance();
public OpenIdConformanceServer getValue(InstanceContext<OpenIdConformanceServer, InjectOpenIdConformanceServer> instanceContext) {
return OpenIdConformanceServer.instance();
}
@Override
@@ -37,13 +37,13 @@ public class OpenIdConformanceSuiteSupplier implements Supplier<OpenIdConformanc
}
@Override
public boolean compatible(InstanceContext<OpenIdConformanceSuite, InjectConformanceSuite> a,
RequestedInstance<OpenIdConformanceSuite, InjectConformanceSuite> b) {
public boolean compatible(InstanceContext<OpenIdConformanceServer, InjectOpenIdConformanceServer> a,
RequestedInstance<OpenIdConformanceServer, InjectOpenIdConformanceServer> b) {
return true;
}
@Override
public void close(InstanceContext<OpenIdConformanceSuite, InjectConformanceSuite> instanceContext) {
public void close(InstanceContext<OpenIdConformanceServer, InjectOpenIdConformanceServer> instanceContext) {
instanceContext.getValue().close();
}
}

View File

@@ -24,5 +24,5 @@ import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface InjectConformanceSuite {
public @interface InjectOpenIdConformanceServer {
}

View File

@@ -0,0 +1,67 @@
# Keycloak OpenID for Verifiable Credentials conformance tests
This module runs Keycloak against the official [OpenID Foundation conformance suite](https://gitlab.com/openid/conformance-suite)
to verify that Keycloak's OpenID for Verifiable Credentials implementation conforms to the specifications. Each test
drives a real conformance suite test module (plan creation, module execution and result assertion) against a live
Keycloak server, so a passing run is evidence of spec conformance, not just internal correctness.
The conformance suite itself (its `mongodb`, `conformance` and `nginx` containers) is started automatically via
Testcontainers; the suite version is pinned by the `conformance.version` property in the root `pom.xml`
(currently `release-v5.2.2`). Keycloak is started by the test framework as a distribution server over HTTPS and
reached by the suite at `https://host.testcontainers.internal:8443`.
## What is covered
The tests are grouped into three JUnit suites, one per profile/role, each with its own Keycloak server
configuration. CI runs every suite as a separate matrix job (`.github/workflows/conformance.yml`), so each
suite gets its own JVM. Running a single suite locally (see [Running the tests](#running-the-tests)) is equivalent.
Note that the combined `./mvnw package` invocation does **not** give per-suite process isolation: Surefire uses a
single reusable fork, so all three suites run in the same JVM.
| Suite | Package | Role / profile | Client authentication |
|-------|---------|----------------|-----------------------|
| `Oid4VciHaipTestSuite` | `vci.haip` | OID4VCI issuer, **HAIP** (`oid4vci-1_0-issuer-haip-test-plan`) | client attestation + DPoP; includes the FAPI2 and mDoc modules |
| `Oid4VciNonHaipTestSuite` | `vci.nonhaip` | OID4VCI issuer, **non-HAIP** (`oid4vci-1_0-issuer-test-plan`, `fapi_profile=vci`) | `private_key_jwt` + DPoP |
| `Oid4VpTestSuite` | `vp` | OID4VP verifier (`oid4vp-1final-verifier-test-plan`) | wallet mock; plain and encrypted (`direct_post.jwt`) response modes |
The suite/profile split is intentional: HAIP requires attestation-based client authentication, whereas the
conformance suite's issuer modules do not permit public or `client_secret` authentication, so the non-HAIP profile
uses `private_key_jwt`. Shared realm building blocks live in `vci/VciConformanceRealmUtil`; each profile's realm and
server configuration lives in `vci/haip` and `vci/nonhaip` respectively.
## Prerequisites
- JDK and Maven (use the `./mvnw` wrapper).
- A running container runtime — **Docker or Podman** — with permission to pull from `registry.gitlab.com`.
- Keycloak built locally so the Quarkus distribution artifact is in your local Maven repository:
```bash
./mvnw install -DskipTests
```
## Running the tests
Run one suite (mirrors how CI runs them, one matrix job each):
```bash
./mvnw package -f tests/conformance/pom.xml -Dtest=Oid4VciNonHaipTestSuite
./mvnw package -f tests/conformance/pom.xml -Dtest=Oid4VciHaipTestSuite
./mvnw package -f tests/conformance/pom.xml -Dtest=Oid4VpTestSuite
```
Run all suites together in one invocation (all three suites share a single Surefire reusable fork, i.e. the same
JVM — this does not reproduce the per-process isolation of the CI matrix jobs):
```bash
./mvnw package -f tests/conformance/pom.xml
```
Run a single test class (fully qualified name; useful when iterating):
```bash
./mvnw test -f tests/conformance/pom.xml \
-Dtest='org.keycloak.tests.conformance.vci.nonhaip.issuer.IssuerHappyFlowTest'
```
The first run pulls the conformance suite container images, so allow extra time. Full logs for a failed conformance
module are printed to the test output to aid debugging.

View File

@@ -52,5 +52,10 @@
<groupId>org.keycloak.testframework</groupId>
<artifactId>keycloak-test-framework-ui</artifactId>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -22,8 +22,8 @@ import java.util.Map;
import java.util.function.Consumer;
import java.util.stream.Stream;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.testframework.conformance.annotations.InjectConformanceSuite;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.conformance.annotations.InjectOpenIdConformanceServer;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleResult;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
@@ -53,8 +53,8 @@ public abstract class AbstractConformanceTest {
@InjectCertificates(config = TlsCertificates.class)
protected ManagedCertificates certificates;
@InjectConformanceSuite
protected OpenIdConformanceSuite suite;
@InjectOpenIdConformanceServer
protected OpenIdConformanceServer server;
protected abstract Stream<ConformanceModuleVariant> moduleVariants();
@@ -72,7 +72,7 @@ public abstract class AbstractConformanceTest {
String name, ConformanceResult expectedResult, BrowserInteraction browserInteraction) {
ConformanceModuleVariant template = new ConformanceModuleVariant(plan, planVariant, name, Map.of(),
expectedResult, browserInteraction);
return OpenIdConformanceSuite.instance().client()
return OpenIdConformanceServer.instance().client()
.discoverModuleVariants(plan, planVariant, name, suiteConfig(template))
.map(moduleVariant -> new ConformanceModuleVariant(plan, planVariant, name, moduleVariant,
expectedResult, browserInteraction));
@@ -86,7 +86,7 @@ public abstract class AbstractConformanceTest {
List<String> names, ConformanceResult expectedResult, BrowserInteraction browserInteraction) {
ConformanceModuleVariant template = new ConformanceModuleVariant(plan, planVariant, names.get(0), Map.of(),
expectedResult, browserInteraction);
Map<String, List<Map<String, String>>> discovered = OpenIdConformanceSuite.instance().client()
Map<String, List<Map<String, String>>> discovered = OpenIdConformanceServer.instance().client()
.discoverModuleVariants(plan, planVariant, names, suiteConfig(template));
return names.stream().flatMap(name -> discovered.get(name).stream()
.map(moduleVariant -> new ConformanceModuleVariant(plan, planVariant, name, moduleVariant,
@@ -104,7 +104,7 @@ public abstract class AbstractConformanceTest {
@ParameterizedTest
@MethodSource("moduleVariants")
public void conformance(ConformanceModuleVariant moduleVariant) {
ConformanceModuleResult result = suite.client()
ConformanceModuleResult result = server.client()
.run(moduleVariant, suiteConfig(moduleVariant), interaction(moduleVariant));
if (!result.finishedWith(moduleVariant.expectedResult())) {
LOGGER.errorf("Full logs of failed conformance module %s:%n%s", result.module(), result.logs().toPrettyString());

View File

@@ -0,0 +1,9 @@
package org.keycloak.tests.conformance.suites;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectPackages({"org.keycloak.tests.conformance.vci.haip"})
public class Oid4VciHaipTestSuite {
}

View File

@@ -0,0 +1,10 @@
package org.keycloak.tests.conformance.suites;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectPackages({"org.keycloak.tests.conformance.vci.nonhaip"})
public class Oid4VciNonHaipTestSuite {
}

View File

@@ -0,0 +1,11 @@
package org.keycloak.tests.conformance.suites;
import org.junit.platform.suite.api.SelectPackages;
import org.junit.platform.suite.api.Suite;
@Suite
@SelectPackages({
"org.keycloak.tests.conformance.vp"
})
public class Oid4VpTestSuite {
}

View File

@@ -22,41 +22,92 @@ import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.function.Consumer;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ModuleRun;
import org.keycloak.tests.conformance.AbstractConformanceTest;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Baseline for OID4VCI conformance tests. Test classes inject a realm with {@link VciConformanceRealmConfig} or
* a subclass of it for additional configuration.
* Baseline for OID4VCI conformance tests, shared by both the HAIP and non-HAIP hierarchies. HAIP test classes inject
* a realm with {@link HaipVciConformanceRealmConfig} (or a subclass); non-HAIP tests extend
* {@link org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest} and inject
* {@link org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig} (or a subclass). Subclasses
* override the JWKS and plan-variant hooks below to supply the profile-specific configuration.
*/
public abstract class AbstractVciConformanceTest extends AbstractConformanceTest {
protected static final String HAIP_PLAN = "oid4vci-1_0-issuer-haip-test-plan";
protected Map<String, String> walletInitiated() {
return planVariant("wallet_initiated");
}
// The plan variant pins every dimension but vci_credential_encryption, leaving the plain and encrypted
// variants to be discovered per module. mdoc is intentionally excluded as Keycloak does not support it.
protected static final Map<String, String> WALLET_INITIATED = Map.of(
"credential_format", "sd_jwt_vc",
"vci_authorization_code_flow_variant", "wallet_initiated");
protected Map<String, String> issuerInitiated() {
return planVariant("issuer_initiated");
}
protected static final Map<String, String> ISSUER_INITIATED = Map.of(
"credential_format", "sd_jwt_vc",
"vci_authorization_code_flow_variant", "issuer_initiated");
/**
* The plan variant selection sent to the conformance suite for the given authorization code flow variant. The
* HAIP plan pins every other dimension itself, so the baseline supplies only the credential format and flow
* variant; {@link org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest} overrides this
* to add the FAPI2 dimensions the generic non-HAIP plan does not pin. Credential encryption is intentionally
* omitted so it stays discovered per module (it defaults to plain).
*/
protected Map<String, String> planVariant(String flowVariant) {
return Map.of(
"credential_format", "sd_jwt_vc",
"vci_authorization_code_flow_variant", flowVariant);
}
@Override
protected JsonNode suiteConfig(ConformanceModuleVariant module) {
return VciSuiteConfig.create(
OpenIdConformanceSuite.KEYCLOAK_BASE_URI,
VciConformanceRealmConfig.credentialConfigurationId(module.planVariant().get("credential_format")),
VciConformanceRealmConfig.attesterJwks(),
OpenIdConformanceServer.KEYCLOAK_BASE_URI,
VciConformanceRealmUtil.credentialConfigurationId(module.planVariant().get("credential_format")),
clientAttesterJwks(),
keyAttestationJwks(),
clientJwks(),
client2Jwks(),
VciTestSigningKey.caCertificatePem(),
module.browserInteraction()).toJson();
}
/**
* The client attester JWKS advertised to the conformance suite for client attestation based authentication.
* HAIP publishes it; non-HAIP uses private_key_jwt and overrides this to {@code null}, which omits the client
* attestation material from the suite config.
*/
protected JsonNode clientAttesterJwks() {
return VciConformanceRealmUtil.attesterJwks();
}
/**
* The key attestation JWKS advertised to the conformance suite for key attestations on the credential proof.
* HAIP publishes it; non-HAIP overrides this to {@code null} by default and re-enables it only in the tests
* that exercise key attestation.
*/
protected JsonNode keyAttestationJwks() {
return VciConformanceRealmUtil.attesterJwks();
}
/**
* The private JWKS the conformance suite signs the first client's private_key_jwt assertions with. HAIP
* authenticates with client attestation and returns {@code null} here (the {@code jwks} client field is then
* omitted); non-HAIP overrides this to publish the conformance client's private key.
*/
protected JsonNode clientJwks() {
return null;
}
/**
* The private JWKS for the second conformance client, which must differ from {@link #clientJwks()} for the
* multiple-clients module. HAIP returns {@code null}; non-HAIP overrides it.
*/
protected JsonNode client2Jwks() {
return null;
}
@Override
protected Consumer<ModuleRun> interaction(ConformanceModuleVariant module) {
if (!"issuer_initiated".equals(module.planVariant().get("vci_authorization_code_flow_variant"))) {
@@ -66,7 +117,7 @@ public abstract class AbstractVciConformanceTest extends AbstractConformanceTest
// endpoint. Keycloak creates the offer through the AIA login, and the suite then fetches it from the
// delivered credential_offer_uri and runs the normal authorization code flow.
String alias = suiteConfig(module).path("alias").asText();
return moduleRun -> suite.client().visitTestEndpoint(alias, "credential_offer",
return moduleRun -> server.client().visitTestEndpoint(alias, "credential_offer",
"credential_offer_uri=" + URLEncoder.encode(createCredentialOfferUri(), StandardCharsets.UTF_8));
}

View File

@@ -25,7 +25,7 @@ import java.nio.charset.StandardCharsets;
import java.time.Duration;
import org.keycloak.representations.idm.oid4vc.VerifiableCredentialOfferActionConfig;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
@@ -36,6 +36,11 @@ import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;
import static org.keycloak.constants.OID4VCIConstants.VERIFIABLE_CREDENTIAL_OFFER_PROVIDER_ID;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.APP_CLIENT;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CREDENTIAL_CONFIGURATION_ID;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.HOLDER;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.PASSWORD;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
/**
* Creates a credential offer through the {@code verifiable_credential_offer} application initiated action, the
@@ -53,7 +58,7 @@ final class VciAiaCredentialOffer {
}
static String createOfferUri() {
URI keycloakUri = OpenIdConformanceSuite.KEYCLOAK_BASE_URI;
URI keycloakUri = OpenIdConformanceServer.KEYCLOAK_BASE_URI;
ChromeOptions options = new ChromeOptions();
options.addArguments(
@@ -71,8 +76,8 @@ final class VciAiaCredentialOffer {
WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10));
wait.until(ExpectedConditions.presenceOfElementLocated(By.id("username")));
driver.findElement(By.id("username")).sendKeys(VciConformanceRealmConfig.HOLDER);
driver.findElement(By.id("password")).sendKeys(VciConformanceRealmConfig.PASSWORD);
driver.findElement(By.id("username")).sendKeys(HOLDER);
driver.findElement(By.id("password")).sendKeys(PASSWORD);
driver.findElement(By.id("kc-login")).click();
WebElement offerLink = wait.until(
@@ -90,13 +95,13 @@ final class VciAiaCredentialOffer {
private static String authorizationUrl(URI keycloakUri) {
VerifiableCredentialOfferActionConfig actionConfig = new VerifiableCredentialOfferActionConfig();
actionConfig.setCredentialConfigurationId(VciConformanceRealmConfig.CREDENTIAL_CONFIGURATION_ID);
actionConfig.setCredentialConfigurationId(CREDENTIAL_CONFIGURATION_ID);
actionConfig.setPreAuthorized(false);
String realmBase = keycloakUri + "/realms/" + VciConformanceRealmConfig.REALM;
String realmBase = keycloakUri + "/realms/" + REALM;
try {
return realmBase + "/protocol/openid-connect/auth"
+ "?client_id=" + VciConformanceRealmConfig.APP_CLIENT
+ "?client_id=" + APP_CLIENT
+ "&redirect_uri=" + encode(realmBase + "/account/")
+ "&response_type=code&scope=openid"
+ "&kc_action=" + encode(VERIFIABLE_CREDENTIAL_OFFER_PROVIDER_ID + ":" + actionConfig.asEncodedParameter());

View File

@@ -22,6 +22,8 @@ import org.keycloak.tests.conformance.ConformanceSigningKey;
import com.fasterxml.jackson.databind.JsonNode;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
/**
* The attester key, generated at runtime so no private key material is committed to the repository. It is signed
* by a CA so it serves both client attestation (verified against the trusted public JWKS) and key attestation
@@ -32,9 +34,9 @@ public final class VciAttesterKey {
static final String KID = "ct_client_attester_key";
// The attestation EKU must match ATTESTER_ATTESTATION_EKU in VciConformanceRealmConfig.
// The attestation EKU must match ATTESTER_ATTESTATION_EKU in HaipVciConformanceRealmConfig.
private static final ConformanceSigningKey KEY = ConformanceSigningKey.generate(
VciConformanceRealmConfig.REALM, KID, "OID4VCI Conformance Attester",
REALM, KID, "OID4VCI Conformance Attester",
KeyPurposeId.id_kp_emailProtection);
private VciAttesterKey() {
@@ -44,7 +46,7 @@ public final class VciAttesterKey {
return KEY.privateJwks();
}
static JsonNode publicJwks() {
public static JsonNode publicJwks() {
return KEY.publicJwks();
}

View File

@@ -25,15 +25,10 @@ import java.util.UUID;
import org.keycloak.OID4VCConstants;
import org.keycloak.VCFormat;
import org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderConfig;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.crypto.Algorithm;
import org.keycloak.crypto.KeyUse;
import org.keycloak.json.RawJsonValue;
import org.keycloak.keys.Attributes;
import org.keycloak.keys.GeneratedEcdhKeyProviderFactory;
import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
@@ -47,37 +42,32 @@ import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCMapper;
import org.keycloak.protocol.oid4vc.issuance.mappers.OID4VCStaticClaimMapper;
import org.keycloak.protocol.oid4vc.model.CredentialScopeRepresentation;
import org.keycloak.protocol.oid4vc.model.DisplayObject;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
import org.keycloak.representations.idm.ClientPolicyRepresentation;
import org.keycloak.representations.idm.ClientProfileRepresentation;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.representations.idm.ProtocolMapperRepresentation;
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
import org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.realm.UserBuilder;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.tests.conformance.ConformanceSigningKey;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.OID4VCConstants.CRYPTOGRAPHIC_BINDING_METHOD_COSE_KEY;
import static org.keycloak.OID4VCConstants.OID4VCI_ENABLED_ATTRIBUTE_KEY;
import static org.keycloak.models.Constants.CREATE_DEFAULT_CLIENT_SCOPES;
import static org.keycloak.models.Constants.DEFAULT_ROLES_ROLE_PREFIX;
public class VciConformanceRealmConfig implements RealmConfig {
/**
* Shared building blocks for the OID4VCI conformance realm. The pieces here are identical for HAIP and non-HAIP:
* realm defaults, the SD-JWT and mDoc credential scopes with their protocol mappers, the holder user, the account
* app client, the signing and encryption key providers, and the common (auth-agnostic) conformance client shape.
*
* The HAIP and non-HAIP realm configs compose these helpers and add only what differs (client authentication,
* client policies, and trust anchors). Kept as a utility rather than a base class so each config reads as a flat
* recipe and the two variants never share behaviour implicitly.
*/
public final class VciConformanceRealmUtil {
public static final String REALM = "oid4vci";
public static final String HOLDER = "alice";
@@ -93,11 +83,22 @@ public class VciConformanceRealmConfig implements RealmConfig {
public static final String MDOC_NAMESPACE = "org.iso.18013.5.1";
// The credential_format plan variant value the conformance suite uses for ISO mdoc, see VCI1FinalCredentialFormat
public static final String MDOC_CREDENTIAL_FORMAT_VARIANT = "mdoc";
public static final String CONFORMANCE_CALLBACK = OpenIdConformanceSuite.INTERNAL_BASE_URI + "/test/a/keycloak/callback";
public static final String TRUST_IDP_ALIAS = "conformance-client-attester";
public static final String X509_TRUST_IDP_ALIAS = "conformance-attester-x509";
// The attester leaf certificate is generated with the emailProtection extended key usage.
private static final String ATTESTER_ATTESTATION_EKU = "1.3.6.1.5.5.7.3.4";
public static final String CONFORMANCE_CALLBACK = OpenIdConformanceServer.INTERNAL_BASE_URI + "/test/a/keycloak/callback";
// FAPI2 requires the TLS layer to only offer BCP195 (RFC 9325) recommended cipher suites for TLS 1.2. The
// default JVM cipher list includes non-recommended suites, which the conformance suite TLS checks reject, so
// the test server is restricted to the recommended AEAD suites. The TLS 1.3 suites must be listed too, as
// restricting the cipher list would otherwise disable TLS 1.3 entirely and the suite checks that TLS 1.3 is
// negotiable where offered. Both the HAIP and non-HAIP servers run the FAPI2-family TLS checks, so they share
// this list.
public static final String TLS_PROTOCOLS = "TLSv1.3,TLSv1.2";
public static final String BCP195_CIPHERS = String.join(",",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
/**
* Maps the conformance suite {@code credential_format} plan variant to the matching credential configuration id
@@ -109,9 +110,17 @@ public class VciConformanceRealmConfig implements RealmConfig {
: CREDENTIAL_CONFIGURATION_ID;
}
@Override
public RealmBuilder configure(RealmBuilder realm) {
realm.name(REALM)
public static JsonNode attesterJwks() {
return VciAttesterKey.privateJwks();
}
/**
* Applies the realm configuration shared by every OID4VCI conformance variant: realm-level attributes, the two
* credential scopes with their protocol mappers, and the holder user. Callers add the clients, client policies,
* trust anchors and key providers that differ per variant.
*/
public static RealmBuilder applyCommon(RealmBuilder realm) {
return realm.name(REALM)
.eventsEnabled(true)
.eventsListeners("jboss-logging")
.verifiableCredentialsEnabled(true)
@@ -138,77 +147,47 @@ public class VciConformanceRealmConfig implements RealmConfig {
.realmRoles(DEFAULT_ROLES_ROLE_PREFIX + "-" + REALM)
.verifiableCredential(SD_JWT_SCOPE)
.verifiableCredential(MDOC_SCOPE)
.build())
.clients(conformanceClient(CLIENT, false), conformanceClient(CLIENT2, true), appClient())
.clientProfile(haipClientProfile())
.clientPolicy(haipClientPolicy())
.update(rep -> {
MultivaluedHashMap<String, ComponentExportRepresentation> components = rep.getComponents();
if (components == null) {
components = new MultivaluedHashMap<>();
rep.setComponents(components);
}
components.add(KeyProvider.class.getName(), conformanceSigningKeyProvider());
components.add(KeyProvider.class.getName(), conformanceEcdhEncryptionKeyProvider());
rep.setIdentityProviders(List.of(attesterTrustIdentityProvider(), attesterX509TrustIdentityProvider()));
});
return realm;
.build());
}
public static JsonNode attesterJwks() {
return VciAttesterKey.privateJwks();
}
private IdentityProviderRepresentation attesterTrustIdentityProvider() {
IdentityProviderRepresentation trust = new IdentityProviderRepresentation();
trust.setAlias(TRUST_IDP_ALIAS);
trust.setProviderId(DefaultTrustIdentityProviderFactory.PROVIDER_ID);
trust.setEnabled(true);
trust.setConfig(Map.of(DefaultTrustIdentityProviderConfig.TRUSTED_JWKS, VciAttesterKey.publicJwks().toString()));
return trust;
}
// Trusts the key attestation x5c certificate chain against the attester CA. Key attestations carry an x5c
// header, so Keycloak must be able to validate the chain against a configured X.509 trust domain.
private IdentityProviderRepresentation attesterX509TrustIdentityProvider() {
IdentityProviderRepresentation trust = new IdentityProviderRepresentation();
trust.setAlias(X509_TRUST_IDP_ALIAS);
trust.setProviderId(DefaultTrustIdentityProviderFactory.PROVIDER_ID);
trust.setEnabled(true);
trust.setConfig(Map.of(
DefaultTrustIdentityProviderConfig.USE_X509, "true",
DefaultTrustIdentityProviderConfig.TRUSTED_CERTIFICATES, VciAttesterKey.caCertificatePem(),
DefaultTrustIdentityProviderConfig.REQUIRED_EXTENDED_KEY_USAGES, ATTESTER_ATTESTATION_EKU));
return trust;
}
private ClientBuilder conformanceClient(String clientId, boolean wildcardRedirect) {
/**
* Common conformance client shape without any client authentication. HAIP and non-HAIP configs add their own
* authentication (attestation-based vs public + PKCE) on top of this.
*/
public static ClientBuilder baseConformanceClient(String clientId, boolean wildcardRedirect) {
return ClientBuilder.create(clientId)
.serviceAccountsEnabled(false)
.directAccessGrantsEnabled(false)
.authenticatorType(AttestationBasedClientAuthenticator.PROVIDER_ID)
.attribute(AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS, TRUST_IDP_ALIAS)
.attribute(OID4VCIConstants.OID4VCI_ATTESTER_TRUST_IDPS_ATTR, TRUST_IDP_ALIAS + "," + X509_TRUST_IDP_ALIAS)
// HAIP requires DPoP sender constraining, so the token endpoint must reject a request without a
// DPoP proof (the holder-of-key conformance module checks exactly this)
.attribute(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS, "true")
.defaultClientScopes("basic", "profile", "roles")
.optionalClientScopes(SD_JWT_SCOPE, MDOC_SCOPE, "email")
.attribute(OID4VCI_ENABLED_ATTRIBUTE_KEY, "true")
.redirectUris(CONFORMANCE_CALLBACK + (wildcardRedirect ? "*" : ""))
.webOrigins(OpenIdConformanceSuite.INTERNAL_BASE_URI.toString());
.webOrigins(OpenIdConformanceServer.INTERNAL_BASE_URI.toString());
}
private ClientBuilder appClient() {
public static ClientBuilder appClient() {
return ClientBuilder.create(APP_CLIENT)
.publicClient(true)
.serviceAccountsEnabled(false)
.directAccessGrantsEnabled(false)
.redirectUris(OpenIdConformanceSuite.KEYCLOAK_BASE_URI + "/realms/" + REALM + "/account/*")
.redirectUris(OpenIdConformanceServer.KEYCLOAK_BASE_URI + "/realms/" + REALM + "/account/*")
.defaultClientScopes("basic", "profile", "roles");
}
private CredentialScopeRepresentation createSdJwtCredentialScope() {
/**
* Adds the signing and ECDH encryption key providers used by every variant to the realm representation.
*/
public static void applyKeyProviders(RealmRepresentation rep) {
MultivaluedHashMap<String, ComponentExportRepresentation> components = rep.getComponents();
if (components == null) {
components = new MultivaluedHashMap<>();
rep.setComponents(components);
}
components.add(KeyProvider.class.getName(), conformanceSigningKeyProvider());
components.add(KeyProvider.class.getName(), conformanceEcdhEncryptionKeyProvider());
}
private static CredentialScopeRepresentation createSdJwtCredentialScope() {
CredentialScopeRepresentation scope = new CredentialScopeRepresentation(SD_JWT_SCOPE)
.setIncludeInTokenScope(true)
.setExpiryInSeconds(300)
@@ -232,7 +211,7 @@ public class VciConformanceRealmConfig implements RealmConfig {
return scope;
}
private CredentialScopeRepresentation createMdocCredentialScope() {
private static CredentialScopeRepresentation createMdocCredentialScope() {
CredentialScopeRepresentation scope = new CredentialScopeRepresentation(MDOC_SCOPE)
.setIncludeInTokenScope(true)
.setExpiryInSeconds(300)
@@ -254,7 +233,7 @@ public class VciConformanceRealmConfig implements RealmConfig {
return scope;
}
private ComponentExportRepresentation conformanceSigningKeyProvider() {
private static ComponentExportRepresentation conformanceSigningKeyProvider() {
ComponentExportRepresentation keyProvider = new ComponentExportRepresentation();
keyProvider.setName("oid4vci-conformance-signing-key");
keyProvider.setId(UUID.randomUUID().toString());
@@ -274,7 +253,7 @@ public class VciConformanceRealmConfig implements RealmConfig {
}
// The conformance suite wallet requests ECDH-ES encrypted credential responses
private ComponentExportRepresentation conformanceEcdhEncryptionKeyProvider() {
private static ComponentExportRepresentation conformanceEcdhEncryptionKeyProvider() {
ComponentExportRepresentation keyProvider = new ComponentExportRepresentation();
keyProvider.setName("oid4vci-conformance-ecdh-encryption-key");
keyProvider.setId(UUID.randomUUID().toString());
@@ -288,7 +267,7 @@ public class VciConformanceRealmConfig implements RealmConfig {
return keyProvider;
}
private List<ProtocolMapperRepresentation> protocolMappers(String scopeName) {
private static List<ProtocolMapperRepresentation> protocolMappers(String scopeName) {
return List.of(
mapper("did-mapper", "oid4vc-subject-id-mapper", Map.of("claim.name", OID4VCConstants.CLAIM_NAME_SUBJECT_ID, "userAttribute", "did")),
mapper("email-mapper", "oid4vc-user-attribute-mapper", Map.of("claim.name", "email", "userAttribute", "email")),
@@ -313,7 +292,7 @@ public class VciConformanceRealmConfig implements RealmConfig {
// 18013-5 marks a fixed set of org.iso.18013.5.1 data elements mandatory for an mDL, so a conformant
// issuer must emit all of them (the suite's EnsureMdocMdlMandatoryDataElementsPresent check enforces this).
// The holder-specific ones come from user attributes; the rest are static document values.
private List<ProtocolMapperRepresentation> mdocProtocolMappers() {
private static List<ProtocolMapperRepresentation> mdocProtocolMappers() {
return List.of(
mdocMapper("did-mapper", "oid4vc-subject-id-mapper", "id", "did"),
mdocMapper("given-name-mapper", "oid4vc-user-attribute-mapper", "given_name", "firstName"),
@@ -329,21 +308,21 @@ public class VciConformanceRealmConfig implements RealmConfig {
mdocStaticMapper("driving-privileges-mapper", "driving_privileges", "[]"));
}
private ProtocolMapperRepresentation mdocStaticMapper(String name, String claimName, String value) {
private static ProtocolMapperRepresentation mdocStaticMapper(String name, String claimName, String value) {
return mapper(name, OID4VCStaticClaimMapper.MAPPER_ID, Map.of(
OID4VCMapper.CLAIM_NAME, claimName,
OID4VCStaticClaimMapper.STATIC_CLAIM_KEY, value,
OID4VCMapper.MDOC_NAMESPACE, MDOC_NAMESPACE));
}
private ProtocolMapperRepresentation mdocMapper(String name, String type, String claimName, String userAttribute) {
private static ProtocolMapperRepresentation mdocMapper(String name, String type, String claimName, String userAttribute) {
return mapper(name, type, Map.of(
OID4VCMapper.CLAIM_NAME, claimName,
OID4VCMapper.USER_ATTRIBUTE_KEY, userAttribute,
OID4VCMapper.MDOC_NAMESPACE, MDOC_NAMESPACE));
}
private ProtocolMapperRepresentation mapper(String name, String type, Map<String, String> config) {
private static ProtocolMapperRepresentation mapper(String name, String type, Map<String, String> config) {
ProtocolMapperRepresentation mapper = new ProtocolMapperRepresentation();
mapper.setName(name);
mapper.setId(UUID.randomUUID().toString());
@@ -352,80 +331,4 @@ public class VciConformanceRealmConfig implements RealmConfig {
mapper.setConfig(config);
return mapper;
}
public static final String HAIP_CLIENT_PROFILE = "oid4vc-haip-profile";
private ClientProfileRepresentation haipClientProfile() {
ClientProfileRepresentation profile = new ClientProfileRepresentation();
profile.setName(HAIP_CLIENT_PROFILE);
profile.setDescription("Enforces the OpenID4VC High Assurance Interoperability Profile 1.0");
profile.setExecutors(List.of(
executor(SecureParContentsExecutorFactory.PROVIDER_ID, JsonNodeFactory.instance.objectNode()),
executor(RejectImplicitGrantExecutorFactory.PROVIDER_ID,
JsonNodeFactory.instance.objectNode().put("auto-configure", false)),
executor(PKCEEnforcerExecutorFactory.PROVIDER_ID,
JsonNodeFactory.instance.objectNode().put("auto-configure", false))));
return profile;
}
private ClientPolicyRepresentation haipClientPolicy() {
ClientPolicyRepresentation policy = new ClientPolicyRepresentation();
policy.setName("oid4vc-haip-policy");
policy.setDescription("Enables the oid4vc-haip-profile for OID4VCI clients");
policy.setEnabled(true);
ClientPolicyConditionRepresentation condition = new ClientPolicyConditionRepresentation();
condition.setConditionProviderId("client-attributes");
ObjectNode config = JsonNodeFactory.instance.objectNode();
config.put("attributes", JsonSerialization.valueAsString(List.of(Map.of(
"key", OID4VCI_ENABLED_ATTRIBUTE_KEY,
"value", String.valueOf(true)))));
condition.setConfiguration(RawJsonValue.of(config));
policy.setConditions(List.of(condition));
policy.setProfiles(List.of(HAIP_CLIENT_PROFILE));
return policy;
}
private ClientPolicyExecutorRepresentation executor(String providerId, JsonNode config) {
ClientPolicyExecutorRepresentation executor = new ClientPolicyExecutorRepresentation();
executor.setExecutorProviderId(providerId);
executor.setConfiguration(RawJsonValue.of(config));
return executor;
}
public static class ConsentRequiredRealmConfig extends VciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return super.configure(realm).update(rep -> rep.getClients().stream()
.filter(client -> CLIENT.equals(client.getClientId()) || CLIENT2.equals(client.getClientId()))
.forEach(client -> client.setConsentRequired(true)));
}
}
public static class ServerConfig implements KeycloakServerConfig {
// FAPI2 requires the TLS layer to only offer BCP195 (RFC 9325) recommended cipher suites for TLS 1.2.
// The default JVM cipher list includes non recommended suites, which the conformance suite TLS checks
// reject, so the test server is restricted to the recommended AEAD suites. The TLS 1.3 suites must be
// listed too, as restricting the cipher list would otherwise disable TLS 1.3 entirely and the suite
// checks that TLS 1.3 is negotiable where offered.
private static final String BCP195_CIPHERS = String.join(",",
"TLS_AES_128_GCM_SHA256",
"TLS_AES_256_GCM_SHA384",
"TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384");
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OID4VC_VCI, Profile.Feature.OID4VC_MDOC, Profile.Feature.CLIENT_AUTH_ABCA)
.option("hostname", OpenIdConformanceSuite.KEYCLOAK_BASE_URI.toString())
.spiOption("keys", "java-keystore", "keystores-path", ConformanceSigningKey.keystoresBaseDir())
.option("https-protocols", "TLSv1.3,TLSv1.2")
.option("https-cipher-suites", BCP195_CIPHERS);
}
}
}

View File

@@ -24,9 +24,16 @@ import org.keycloak.testframework.conformance.runner.BrowserFlow;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.JsonNode;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT2;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CONFORMANCE_CALLBACK;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.HOLDER;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.PASSWORD;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
/**
* The test configuration uploaded to the conformance suite. The structure is defined by the suite and issuer
@@ -40,29 +47,37 @@ record VciSuiteConfig(
Client client2,
List<BrowserFlow> browser) {
static VciSuiteConfig create(URI keycloakBaseUri, String credentialConfigurationId, JsonNode attesterJwks,
String trustAnchorPem, BrowserInteraction browserInteraction) {
static VciSuiteConfig create(URI keycloakBaseUri, String credentialConfigurationId, JsonNode clientAttesterJwks,
JsonNode keyAttestationJwks, JsonNode clientJwks, JsonNode client2Jwks, String trustAnchorPem,
BrowserInteraction browserInteraction) {
// Client attestation (HAIP) and key attestation (HAIP and non-HAIP key-attestation tests) are independent:
// the client_attestation_* fields are only emitted when a client attester JWKS is supplied, so a non-HAIP
// private_key_jwt client that still exercises key attestation advertises only key_attestation_jwks.
String clientAttestationIssuer = clientAttesterJwks == null ? null : "https://example.com/client-attester";
return new VciSuiteConfig(
"keycloak",
new Vci(keycloakBaseUri + "/realms/" + VciConformanceRealmConfig.REALM,
new Vci(keycloakBaseUri + "/realms/" + REALM,
credentialConfigurationId,
"https://example.com/client-attester",
attesterJwks,
attesterJwks),
clientAttestationIssuer,
clientAttesterJwks,
keyAttestationJwks),
new Credential(trustAnchorPem, trustAnchorPem),
new Client(VciConformanceRealmConfig.CLIENT),
new Client(VciConformanceRealmConfig.CLIENT2),
// HAIP passes null client JWKS (it authenticates with client attestation); non-HAIP passes the
// distinct private JWKS the suite signs each client's private_key_jwt assertions with.
new Client(CLIENT, clientJwks),
new Client(CLIENT2, client2Jwks),
browserInteraction.browserFlows(new BrowserInteraction.BrowserContext(
VciConformanceRealmConfig.REALM,
VciConformanceRealmConfig.HOLDER,
VciConformanceRealmConfig.PASSWORD,
VciConformanceRealmConfig.CONFORMANCE_CALLBACK)));
REALM,
HOLDER,
PASSWORD,
CONFORMANCE_CALLBACK)));
}
JsonNode toJson() {
return JsonSerialization.writeValueAsNode(this);
}
@JsonInclude(JsonInclude.Include.NON_NULL)
record Vci(
@JsonProperty("credential_issuer_url") String credentialIssuerUrl,
@JsonProperty("credential_configuration_id") String credentialConfigurationId,
@@ -76,6 +91,9 @@ record VciSuiteConfig(
@JsonProperty("status_list_trust_anchor_pem") String statusListTrustAnchorPem) {
}
record Client(@JsonProperty("client_id") String clientId) {
@JsonInclude(JsonInclude.Include.NON_NULL)
record Client(
@JsonProperty("client_id") String clientId,
@JsonProperty("jwks") JsonNode jwks) {
}
}

View File

@@ -47,6 +47,8 @@ import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
// Not using org.keycloak.common.util.CertificateUtils as its leaf certificates are CA capable, which strict
// trust chain validation rejects for an issuer signing certificate
final class VciTestSigningKey {
@@ -79,7 +81,7 @@ final class VciTestSigningKey {
new Certificate[] { leafCertificate, caCertificate });
Path keyStorePath = Files.createTempFile(
ConformanceSigningKey.realmKeystoreDir(VciConformanceRealmConfig.REALM),
ConformanceSigningKey.realmKeystoreDir(REALM),
"keycloak-oid4vci-conformance-signing", ".p12");
try (OutputStream output = Files.newOutputStream(keyStorePath)) {
keyStore.store(output, PASSWORD.toCharArray());

View File

@@ -0,0 +1,182 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.haip;
import java.util.List;
import java.util.Map;
import org.keycloak.authentication.authenticators.client.AttestationBasedClientAuthenticator;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderConfig;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.json.RawJsonValue;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.ClientPolicyConditionRepresentation;
import org.keycloak.representations.idm.ClientPolicyExecutorRepresentation;
import org.keycloak.representations.idm.ClientPolicyRepresentation;
import org.keycloak.representations.idm.ClientProfileRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.services.clientpolicy.executor.PKCEEnforcerExecutorFactory;
import org.keycloak.services.clientpolicy.executor.RejectImplicitGrantExecutorFactory;
import org.keycloak.services.clientpolicy.executor.SecureParContentsExecutorFactory;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.tests.conformance.ConformanceSigningKey;
import org.keycloak.tests.conformance.vci.VciAttesterKey;
import org.keycloak.tests.conformance.vci.VciConformanceRealmUtil;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import org.keycloak.util.JsonSerialization;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
import com.fasterxml.jackson.databind.node.ObjectNode;
import static org.keycloak.OID4VCConstants.OID4VCI_ENABLED_ATTRIBUTE_KEY;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT2;
/**
* HAIP variant of the OID4VCI conformance realm. It composes the shared realm building blocks from
* {@link VciConformanceRealmUtil} and adds the High Assurance Interoperability Profile specifics: attestation-based
* client authentication with DPoP sender constraining, the client attestation trust anchors, and the HAIP client
* profile/policy (SecurePAR, no implicit grant, PKCE enforced). The non-HAIP variant lives in
* {@link NonHaipVciConformanceRealmConfig}.
*/
public class HaipVciConformanceRealmConfig implements RealmConfig {
public static final String HAIP_PLAN = "oid4vci-1_0-issuer-haip-test-plan";
public static final String HAIP_CLIENT_PROFILE = "oid4vc-haip-profile";
public static final String TRUST_IDP_ALIAS = "conformance-client-attester";
public static final String X509_TRUST_IDP_ALIAS = "conformance-attester-x509";
// The attester leaf certificate is generated with the emailProtection extended key usage.
private static final String ATTESTER_ATTESTATION_EKU = "1.3.6.1.5.5.7.3.4";
@Override
public RealmBuilder configure(RealmBuilder realm) {
VciConformanceRealmUtil.applyCommon(realm)
.clients(conformanceClient(CLIENT, false), conformanceClient(CLIENT2, true), VciConformanceRealmUtil.appClient())
.clientProfile(haipClientProfile())
.clientPolicy(haipClientPolicy())
.update(rep -> {
VciConformanceRealmUtil.applyKeyProviders(rep);
rep.setIdentityProviders(List.of(attesterTrustIdentityProvider(), attesterX509TrustIdentityProvider()));
});
return realm;
}
// Attestation-based client authentication against the trust IDPs plus DPoP sender constraining.
private ClientBuilder conformanceClient(String clientId, boolean wildcardRedirect) {
return VciConformanceRealmUtil.baseConformanceClient(clientId, wildcardRedirect)
.authenticatorType(AttestationBasedClientAuthenticator.PROVIDER_ID)
.attribute(AttestationBasedClientAuthenticator.OAUTH_CLIENT_ATTESTATION_CONFIG_TRUST_IDPS, TRUST_IDP_ALIAS)
.attribute(OID4VCIConstants.OID4VCI_ATTESTER_TRUST_IDPS_ATTR, TRUST_IDP_ALIAS + "," + X509_TRUST_IDP_ALIAS)
// HAIP requires DPoP sender constraining, so the token endpoint must reject a request without a
// DPoP proof (the holder-of-key conformance module checks exactly this)
.attribute(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS, "true");
}
private IdentityProviderRepresentation attesterTrustIdentityProvider() {
IdentityProviderRepresentation trust = new IdentityProviderRepresentation();
trust.setAlias(TRUST_IDP_ALIAS);
trust.setProviderId(DefaultTrustIdentityProviderFactory.PROVIDER_ID);
trust.setEnabled(true);
trust.setConfig(Map.of(DefaultTrustIdentityProviderConfig.TRUSTED_JWKS, VciAttesterKey.publicJwks().toString()));
return trust;
}
// Trusts the key attestation x5c certificate chain against the attester CA. Key attestations carry an x5c
// header, so Keycloak must be able to validate the chain against a configured X.509 trust domain.
private IdentityProviderRepresentation attesterX509TrustIdentityProvider() {
IdentityProviderRepresentation trust = new IdentityProviderRepresentation();
trust.setAlias(X509_TRUST_IDP_ALIAS);
trust.setProviderId(DefaultTrustIdentityProviderFactory.PROVIDER_ID);
trust.setEnabled(true);
trust.setConfig(Map.of(
DefaultTrustIdentityProviderConfig.USE_X509, "true",
DefaultTrustIdentityProviderConfig.TRUSTED_CERTIFICATES, VciAttesterKey.caCertificatePem(),
DefaultTrustIdentityProviderConfig.REQUIRED_EXTENDED_KEY_USAGES, ATTESTER_ATTESTATION_EKU));
return trust;
}
private ClientProfileRepresentation haipClientProfile() {
ClientProfileRepresentation profile = new ClientProfileRepresentation();
profile.setName(HAIP_CLIENT_PROFILE);
profile.setDescription("Enforces the OpenID4VC High Assurance Interoperability Profile 1.0");
profile.setExecutors(List.of(
executor(SecureParContentsExecutorFactory.PROVIDER_ID, JsonNodeFactory.instance.objectNode()),
executor(RejectImplicitGrantExecutorFactory.PROVIDER_ID,
JsonNodeFactory.instance.objectNode().put("auto-configure", false)),
executor(PKCEEnforcerExecutorFactory.PROVIDER_ID,
JsonNodeFactory.instance.objectNode().put("auto-configure", false))));
return profile;
}
private ClientPolicyRepresentation haipClientPolicy() {
ClientPolicyRepresentation policy = new ClientPolicyRepresentation();
policy.setName("oid4vc-haip-policy");
policy.setDescription("Enables the oid4vc-haip-profile for OID4VCI clients");
policy.setEnabled(true);
ClientPolicyConditionRepresentation condition = new ClientPolicyConditionRepresentation();
condition.setConditionProviderId("client-attributes");
ObjectNode config = JsonNodeFactory.instance.objectNode();
config.put("attributes", JsonSerialization.valueAsString(List.of(Map.of(
"key", OID4VCI_ENABLED_ATTRIBUTE_KEY,
"value", String.valueOf(true)))));
condition.setConfiguration(RawJsonValue.of(config));
policy.setConditions(List.of(condition));
policy.setProfiles(List.of(HAIP_CLIENT_PROFILE));
return policy;
}
private ClientPolicyExecutorRepresentation executor(String providerId, JsonNode config) {
ClientPolicyExecutorRepresentation executor = new ClientPolicyExecutorRepresentation();
executor.setExecutorProviderId(providerId);
executor.setConfiguration(RawJsonValue.of(config));
return executor;
}
public static class ConsentRequiredRealmConfig extends HaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return super.configure(realm).update(rep -> rep.getClients().stream()
.filter(client -> CLIENT.equals(client.getClientId()) || CLIENT2.equals(client.getClientId()))
.forEach(client -> client.setConsentRequired(true)));
}
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OID4VC_VCI, Profile.Feature.OID4VC_MDOC, Profile.Feature.CLIENT_AUTH_ABCA)
.option("https-protocols", VciConformanceRealmUtil.TLS_PROTOCOLS)
.option("https-cipher-suites", VciConformanceRealmUtil.BCP195_CIPHERS)
.option("hostname", OpenIdConformanceServer.KEYCLOAK_BASE_URI.toString())
.spiOption("keys", "java-keystore", "keystores-path", ConformanceSigningKey.keystoresBaseDir());
}
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -29,9 +29,11 @@ import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerExpiredRequestUriTest extends AbstractVciConformanceTest {
@InjectRealm(config = ShortParRequestUriLifespanRealmConfig.class, lifecycle = LifeCycle.METHOD)
@@ -41,14 +43,14 @@ public class IssuerExpiredRequestUriTest extends AbstractVciConformanceTest {
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-par-attempt-to-use-expired-request_uri",
ConformanceResult.REVIEW,
// the expired request_uri is no longer resolvable, so Keycloak rejects it at the authorization endpoint
BrowserInteraction.errorPage("PAR not found, not issued or used multiple times."));
}
public static class ShortParRequestUriLifespanRealmConfig extends VciConformanceRealmConfig {
public static class ShortParRequestUriLifespanRealmConfig extends HaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.List;
import java.util.stream.Stream;
@@ -28,9 +28,11 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2LoginTest extends AbstractVciConformanceTest {
private static final List<String> MODULES = List.of(
@@ -60,12 +62,12 @@ public class IssuerFapi2LoginTest extends AbstractVciConformanceTest {
"fapi2-security-profile-final-plain-fapi-tolerate-unregistered-redirect-uri",
"fapi2-security-profile-final-ensure-different-state-inside-and-outside-request-object");
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(HAIP_PLAN, WALLET_INITIATED, MODULES,
return discoverModuleVariants(HAIP_PLAN, walletInitiated(), MODULES,
ConformanceResult.PASSED, BrowserInteraction.LOGIN);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.List;
import java.util.stream.Stream;
@@ -28,9 +28,11 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2NoBrowserTest extends AbstractVciConformanceTest {
private static final List<String> MODULES = List.of(
@@ -39,12 +41,12 @@ public class IssuerFapi2NoBrowserTest extends AbstractVciConformanceTest {
"fapi2-security-profile-final-par-attempt-invalid-http-method",
"fapi2-security-profile-final-ensure-request-object-without-redirect-uri-fails");
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(HAIP_PLAN, WALLET_INITIATED, MODULES,
return discoverModuleVariants(HAIP_PLAN, walletInitiated(), MODULES,
ConformanceResult.PASSED, BrowserInteraction.NONE);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.List;
import java.util.stream.Stream;
@@ -28,25 +28,27 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Sends authorization requests with missing or plain PKCE: Keycloak rejects them at the authorization endpoint
* and redirects the error to the callback without ever presenting a login, where the module validates it.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2PkceRejectedTest extends AbstractVciConformanceTest {
private static final List<String> MODULES = List.of(
"fapi2-security-profile-final-par-ensure-pkce-required",
"fapi2-security-profile-final-par-plain-pkce-rejected");
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(HAIP_PLAN, WALLET_INITIATED, MODULES,
return discoverModuleVariants(HAIP_PLAN, walletInitiated(), MODULES,
ConformanceResult.PASSED, BrowserInteraction.ERROR_CALLBACK);
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,23 +27,25 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* One client pushes a request, then a different client presents the same request_uri at the authorization
* endpoint: Keycloak must reject it because the pushed request is bound to the client that created it.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2RequestUriForDifferentClientTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-par-attempt-to-use-request_uri-for-different-client",
ConformanceResult.REVIEW,
// the second client's client_id does not match the one bound to the pushed request_uri

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2RequestUriReusePriorTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-par-ensure-reused-request-uri-prior-to-auth-completion-succeeds",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN_ON_SECOND_VISIT);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,23 +27,25 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Uses a request_uri once (login completes), then reuses the same request_uri: Keycloak must reject the reuse
* because the pushed request has been consumed.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2ReuseRequestUriTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-par-attempt-reuse-request_uri",
ConformanceResult.REVIEW,
BrowserInteraction.loginThenErrorPageOnSecondVisit("PAR not found, not issued or used multiple times."));

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,23 +27,25 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Sends an authorization request that carries state only outside the request object: Keycloak requires the
* request to go through PAR, so it rejects the plain authorization request.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2StateOutsideRequestObjectTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-state-only-outside-request-object-not-used",
ConformanceResult.REVIEW,
BrowserInteraction.errorPage("PAR request did not include query parameter"));

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,23 +27,25 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Sends an unsigned authorization request without going through PAR: Keycloak requires pushed authorization, so
* it rejects the request because it has no pushed request_uri to resolve.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerFapi2UnsignedRequestWithoutParTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-ensure-unsigned-authorization-request-without-using-par-fails",
ConformanceResult.REVIEW,
BrowserInteraction.errorPage("PAR request_uri not included."));

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHolderOfKeyRequiredTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-ensure-holder-of-key-required",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerRefreshTokenTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-refresh-token",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.fapi2;
package org.keycloak.tests.conformance.vci.haip.fapi2;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUserRejectsAuthenticationTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.ConsentRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.ConsentRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"fapi2-security-profile-final-user-rejects-authentication",
ConformanceResult.PASSED,
BrowserInteraction.DENY_CONSENT);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerAccessTokenInQueryTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-on-access-token-in-query",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -29,9 +29,11 @@ import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerBatchIssuanceTest extends AbstractVciConformanceTest {
@InjectRealm(config = BatchIssuanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@@ -41,13 +43,13 @@ public class IssuerBatchIssuanceTest extends AbstractVciConformanceTest {
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-batch-issuance",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
public static class BatchIssuanceRealmConfig extends VciConformanceRealmConfig {
public static class BatchIssuanceRealmConfig extends HaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerClientAttestationExpInPastTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-client-attestation-exp-in-past",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerClientAttestationNoSubTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-client-attestation-no-sub",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerClientAttestationPopWrongAudTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-client-attestation-pop-wrong-aud",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowAdditionalRequestsTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-additional-requests",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowMultipleClientsTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-multiple-clients",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowSkipNotificationTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-skip-notification",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,9 +27,11 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedBatchIssuanceTest extends AbstractVciConformanceTest {
@InjectRealm(config = IssuerBatchIssuanceTest.BatchIssuanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@@ -39,7 +41,7 @@ public class IssuerInitiatedBatchIssuanceTest extends AbstractVciConformanceTest
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
ISSUER_INITIATED,
issuerInitiated(),
"oid4vci-1_0-issuer-batch-issuance",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,24 +27,26 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* The issuer_initiated flow: Keycloak creates a credential offer through the verifiable_credential_offer
* application initiated action and the suite receives it, fetches the offer from its credential_offer_uri and
* completes the authorization code flow with its own wallet client.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedHappyFlowTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
ISSUER_INITIATED,
issuerInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN)

View File

@@ -15,25 +15,27 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import org.junit.jupiter.api.Disabled;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
// TODO: enable once https://github.com/keycloak/keycloak/issues/48188 is fixed. The REST endpoint binds the
// offer to the client of the access token used to create it, and redeeming an offer with a different client is
// rejected ("Unexpected login client"). The suite redeems with its own wallet client, so the offer must be
// created without a target client, which only the AIA path (see IssuerInitiatedHappyFlowTest) supports today.
@Disabled("The create-credential-offer REST endpoint binds the offer to the creating client, see the TODO")
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedRestOfferTest extends IssuerInitiatedHappyFlowTest {
@Override
protected String createCredentialOfferUri() {
throw new UnsupportedOperationException(
"Create the offer via POST /realms/" + VciConformanceRealmConfig.REALM
"Create the offer via POST /realms/" + REALM
+ "/protocol/oid4vc/create-credential-offer once offers are no longer bound to the creating client");
}
}

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidClientAttestationPopSignatureTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-client-attestation-pop-signature",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidClientAttestationSignatureTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-client-attestation-signature",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidJwtProofSignatureTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-jwt-proof-signature",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,9 +27,11 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidKeyAttestationSignatureTest extends AbstractVciConformanceTest {
@InjectRealm(config = IssuerKeyAttestationTest.KeyAttestationRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
@@ -39,7 +41,7 @@ public class IssuerInvalidKeyAttestationSignatureTest extends AbstractVciConform
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-key-attestation-signature",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidNonceTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-nonce",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -29,14 +29,17 @@ import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.SD_JWT_SCOPE;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Issues a credential whose configuration requires key attestations, so the suite includes a valid key attestation
* that Keycloak must accept. The attestation x5c chain is trusted through the {@code conformance-attester-x509}
* trust-material identity provider configured by {@link VciConformanceRealmConfig}.
* trust-material identity provider configured by {@link HaipVciConformanceRealmConfig}.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerKeyAttestationTest extends AbstractVciConformanceTest {
@InjectRealm(config = KeyAttestationRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
@@ -46,13 +49,13 @@ public class IssuerKeyAttestationTest extends AbstractVciConformanceTest {
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
public static class KeyAttestationRequiredRealmConfig extends VciConformanceRealmConfig {
public static class KeyAttestationRequiredRealmConfig extends HaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerMismatchedClientAttestationPopKeyTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-mismatched-client-attestation-pop-key",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerMissingProofTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-missing-proof",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerSignedMetadataTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-metadata-test-signed",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnknownCredentialConfigurationTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-unknown-credential-configuration",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnknownCredentialIdentifierTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-unknown-credential-identifier",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnsignedMetadataTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-metadata-test",
ConformanceResult.PASSED,
BrowserInteraction.NONE);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.issuer;
package org.keycloak.tests.conformance.vci.haip.issuer;
import java.util.stream.Stream;
@@ -27,19 +27,21 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.VciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnsupportedEncryptionAlgorithmTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
HAIP_PLAN,
WALLET_INITIATED,
walletInitiated(),
"oid4vci-1_0-issuer-fail-unsupported-encryption-algorithm",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);

View File

@@ -15,7 +15,7 @@
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci;
package org.keycloak.tests.conformance.vci.haip.mdoc;
import java.util.Map;
import java.util.stream.Stream;
@@ -27,23 +27,29 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import org.keycloak.tests.conformance.vci.haip.issuer.IssuerHappyFlowTest;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.MDOC_CREDENTIAL_FORMAT_VARIANT;
import static org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig.HAIP_PLAN;
/**
* Runs the issuer happy flow with the ISO mdoc credential format, mirroring {@link org.keycloak.tests.conformance.vci.issuer.IssuerHappyFlowTest} for SD-JWT VC.
* Runs the issuer happy flow with the ISO mdoc credential format, mirroring {@link IssuerHappyFlowTest} for SD-JWT VC.
*/
@KeycloakIntegrationTest(config = VciConformanceRealmConfig.ServerConfig.class)
@KeycloakIntegrationTest(config = HaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerMdocHappyFlowTest extends AbstractVciConformanceTest {
@InjectRealm(config = VciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
@InjectRealm(config = HaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
// The plan variant pins every dimension but vci_credential_encryption, leaving the plain and encrypted variants
return discoverModuleVariants(
"oid4vci-1_0-issuer-haip-test-plan",
HAIP_PLAN,
Map.of(
"credential_format", VciConformanceRealmConfig.MDOC_CREDENTIAL_FORMAT_VARIANT,
"credential_format", MDOC_CREDENTIAL_FORMAT_VARIANT,
"vci_authorization_code_flow_variant", "wallet_initiated"),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip;
import java.util.HashMap;
import java.util.Map;
import org.keycloak.tests.conformance.vci.AbstractVciConformanceTest;
import com.fasterxml.jackson.databind.JsonNode;
/**
* Baseline for non-HAIP OID4VCI conformance tests. Non-HAIP profiles authenticate with private_key_jwt and use
* neither client attestation nor key attestation, so the suite config advertises the client's private JWKS instead
* of any attester JWKS.
*
* Unlike the HAIP plan, which pins every FAPI2/VCI variant dimension inside the plan, the non-HAIP plan
* ({@code oid4vci-1_0-issuer-test-plan}) pins nothing, so the caller must supply the full variant selection. This
* overrides {@link #planVariant(String)} to add the non-HAIP FAPI2 dimensions (profile {@code vci},
* private_key_jwt, DPoP, unsigned PAR request, plain OAuth) on top of the credential format and flow variant.
* Credential encryption is intentionally omitted so it stays discovered per module (it defaults to {@code plain}).
*/
public abstract class AbstractNonHaipVciConformanceTest extends AbstractVciConformanceTest {
@Override
protected Map<String, String> planVariant(String flowVariant) {
return Map.of(
"credential_format", "sd_jwt_vc",
"vci_authorization_code_flow_variant", flowVariant,
"fapi_profile", "vci",
"client_auth_type", "private_key_jwt",
"fapi_request_method", "unsigned",
"openid", "plain_oauth",
"sender_constrain", "dpop",
"fapi_response_mode", "plain_response");
}
/**
* Adds {@code vci_credential_encryption=encrypted} to a plan variant. The generic non-HAIP plan does not pin
* credential encryption (it defaults to plain), so tests that must exercise the encrypted credential response
* request it explicitly.
*/
protected Map<String, String> encrypted(Map<String, String> planVariant) {
Map<String, String> variant = new HashMap<>(planVariant);
variant.put("vci_credential_encryption", "encrypted");
return variant;
}
// Non-HAIP authenticates with private_key_jwt, so it advertises no client attestation material.
@Override
protected JsonNode clientAttesterJwks() {
return null;
}
// Key attestation is off by default; the key-attestation tests re-enable it.
@Override
protected JsonNode keyAttestationJwks() {
return null;
}
@Override
protected JsonNode clientJwks() {
return VciClientKey.privateJwks();
}
@Override
protected JsonNode client2Jwks() {
return VciClientKey.privateJwks2();
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip;
import java.util.List;
import java.util.Map;
import org.keycloak.authentication.authenticators.client.JWTClientAuthenticator;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderConfig;
import org.keycloak.broker.trust.DefaultTrustIdentityProviderFactory;
import org.keycloak.common.Profile;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.protocol.oidc.OIDCConfigAttributes;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
import org.keycloak.testframework.server.KeycloakServerConfig;
import org.keycloak.testframework.server.KeycloakServerConfigBuilder;
import org.keycloak.tests.conformance.ConformanceSigningKey;
import org.keycloak.tests.conformance.vci.VciAttesterKey;
import org.keycloak.tests.conformance.vci.VciConformanceRealmUtil;
import org.keycloak.tests.conformance.vci.haip.HaipVciConformanceRealmConfig;
import com.fasterxml.jackson.databind.JsonNode;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.CLIENT2;
/**
* Non-HAIP variant of the OID4VCI conformance realm. It composes the shared realm building blocks from
* {@link VciConformanceRealmUtil} and, unlike {@link HaipVciConformanceRealmConfig}, authenticates the conformance
* clients with private_key_jwt (confidential clients holding a registered public JWKS) instead of attestation-based
* client authentication, so there are no client attestation trust anchors and no HAIP client profile/policy.
*
* The conformance suite's OID4VCI issuer test modules do not permit public clients or client_secret authentication
* ({@code client_auth_type=none}/{@code client_secret_*} are marked not applicable), so private_key_jwt is the
* non-HAIP client authentication used here. FAPI2 still mandates sender constraining, so the clients keep DPoP.
*
* Pairs with its own {@link ServerConfig}, the neutral OID4VC_VCI server baseline (no CLIENT_AUTH_ABCA), since
* non-HAIP does not use attestation-based client authentication.
*/
public class NonHaipVciConformanceRealmConfig implements RealmConfig {
public static final String NON_HAIP_PLAN = "oid4vci-1_0-issuer-test-plan";
// Key attestations carry an x5c chain, so Keycloak validates them against this X.509 trust domain. Client
// attestation (HAIP) is not used here, only key attestation on the proof, so there is no JWKS trust IDP.
public static final String X509_TRUST_IDP_ALIAS = "conformance-attester-x509";
// The attester leaf certificate is generated with the emailProtection extended key usage.
private static final String ATTESTER_ATTESTATION_EKU = "1.3.6.1.5.5.7.3.4";
@Override
public RealmBuilder configure(RealmBuilder realm) {
VciConformanceRealmUtil.applyCommon(realm)
.clients(conformanceClient(CLIENT, false, VciClientKey.publicJwks()),
conformanceClient(CLIENT2, true, VciClientKey.publicJwks2()),
VciConformanceRealmUtil.appClient())
.update(rep -> {
VciConformanceRealmUtil.applyKeyProviders(rep);
rep.setIdentityProviders(List.of(attesterX509TrustIdentityProvider()));
});
return realm;
}
// Non-HAIP client authentication: confidential client using private_key_jwt with the conformance client's own
// public JWKS registered on the client, plus DPoP sender constraining as FAPI2 requires. The suite holds the
// matching private JWKS (see VciClientKey / VciSuiteConfig) to sign the client assertions. The attester trust
// IDP is referenced so key attestations on the proof can be validated.
private ClientBuilder conformanceClient(String clientId, boolean wildcardRedirect, JsonNode publicJwks) {
return VciConformanceRealmUtil.baseConformanceClient(clientId, wildcardRedirect)
.authenticatorType(JWTClientAuthenticator.PROVIDER_ID)
.attribute(OIDCConfigAttributes.USE_JWKS_STRING, "true")
.attribute(OIDCConfigAttributes.JWKS_STRING, publicJwks.toString())
.attribute(OID4VCIConstants.OID4VCI_ATTESTER_TRUST_IDPS_ATTR, X509_TRUST_IDP_ALIAS)
.attribute(OIDCConfigAttributes.DPOP_BOUND_ACCESS_TOKENS, "true");
}
// Trusts the key attestation x5c certificate chain against the attester CA, so Keycloak can validate the chain
// against a configured X.509 trust domain when a credential configuration requires key attestation.
private IdentityProviderRepresentation attesterX509TrustIdentityProvider() {
IdentityProviderRepresentation trust = new IdentityProviderRepresentation();
trust.setAlias(X509_TRUST_IDP_ALIAS);
trust.setProviderId(DefaultTrustIdentityProviderFactory.PROVIDER_ID);
trust.setEnabled(true);
trust.setConfig(Map.of(
DefaultTrustIdentityProviderConfig.USE_X509, "true",
DefaultTrustIdentityProviderConfig.TRUSTED_CERTIFICATES, VciAttesterKey.caCertificatePem(),
DefaultTrustIdentityProviderConfig.REQUIRED_EXTENDED_KEY_USAGES, ATTESTER_ATTESTATION_EKU));
return trust;
}
public static class ServerConfig implements KeycloakServerConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OID4VC_VCI)
.option("https-protocols", VciConformanceRealmUtil.TLS_PROTOCOLS)
.option("https-cipher-suites", VciConformanceRealmUtil.BCP195_CIPHERS)
.option("hostname", OpenIdConformanceServer.KEYCLOAK_BASE_URI.toString())
.spiOption("keys", "java-keystore", "keystores-path", ConformanceSigningKey.keystoresBaseDir());
}
}
}

View File

@@ -0,0 +1,66 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip;
import org.keycloak.tests.conformance.ConformanceSigningKey;
import org.keycloak.tests.conformance.vci.VciAttesterKey;
import com.fasterxml.jackson.databind.JsonNode;
import org.bouncycastle.asn1.x509.KeyPurposeId;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
/**
* The client signing key for non-HAIP private_key_jwt client authentication, generated at runtime so no private
* key material is committed to the repository. The conformance suite (acting as the wallet client) holds the
* private JWKS to sign client assertions, while Keycloak registers only the public JWKS on the client. Unlike
* {@link VciAttesterKey} there is no x5c chain validation for private_key_jwt: Keycloak verifies the assertion
* signature against the registered public JWKS, so the leaf certificate extended key usage is immaterial here.
*/
public final class VciClientKey {
// Two independent keys, one per conformance client. The suite's multiple-clients module asserts the two
// clients authenticate with different private keys (ValidateClientPrivateKeysAreDifferent), so the keys must
// not be shared.
private static final ConformanceSigningKey KEY = ConformanceSigningKey.generate(
REALM, "ct_client_signing_key", "OID4VCI Conformance Client",
KeyPurposeId.id_kp_clientAuth);
private static final ConformanceSigningKey KEY2 = ConformanceSigningKey.generate(
REALM, "ct_client2_signing_key", "OID4VCI Conformance Client 2",
KeyPurposeId.id_kp_clientAuth);
private VciClientKey() {
}
static JsonNode privateJwks() {
return KEY.privateJwks();
}
static JsonNode publicJwks() {
return KEY.publicJwks();
}
static JsonNode privateJwks2() {
return KEY2.privateJwks();
}
static JsonNode publicJwks2() {
return KEY2.publicJwks();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerAccessTokenInQueryTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-on-access-token-in-query",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.constants.OID4VCIConstants;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerBatchIssuanceTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = BatchIssuanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-batch-issuance",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
public static class BatchIssuanceRealmConfig extends NonHaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return super.configure(realm).attribute(OID4VCIConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE, "20");
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowAdditionalRequestsTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-additional-requests",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowMultipleClientsTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-multiple-clients",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowSkipNotificationTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow-skip-notification",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerHappyFlowTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedBatchIssuanceTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = IssuerBatchIssuanceTest.BatchIssuanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
issuerInitiated(),
"oid4vci-1_0-issuer-batch-issuance",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,61 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
/**
* The issuer_initiated flow: Keycloak creates a credential offer through the verifiable_credential_offer
* application initiated action and the suite receives it, fetches the offer from its credential_offer_uri and
* completes the authorization code flow with its own wallet client.
*/
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedHappyFlowTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
issuerInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN)
// TODO (#50889): include the encrypted variant once Keycloak keeps the credential offer state for the
// lifetime of the authorized session. Keycloak removes the offer state after the first
// successful issuance (OID4VCIssuerEndpoint), so the encrypted variant's second credential
// request (encryption + DEFLATE compression check) fails with "No credential offer state".
// The generic non-HAIP plan does not pin credential encryption, so the discovered variant may
// omit the key entirely (defaults to plain); exclude only the explicit encrypted variant.
.filter(module -> !"encrypted".equals(module.moduleVariant().get("vci_credential_encryption")));
}
}

View File

@@ -0,0 +1,41 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import org.junit.jupiter.api.Disabled;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.REALM;
// TODO: enable once https://github.com/keycloak/keycloak/issues/48188 is fixed. The REST endpoint binds the
// offer to the client of the access token used to create it, and redeeming an offer with a different client is
// rejected ("Unexpected login client"). The suite redeems with its own wallet client, so the offer must be
// created without a target client, which only the AIA path (see IssuerInitiatedHappyFlowTest) supports today.
@Disabled("The create-credential-offer REST endpoint binds the offer to the creating client, see the TODO")
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInitiatedRestOfferTest extends IssuerInitiatedHappyFlowTest {
@Override
protected String createCredentialOfferUri() {
throw new UnsupportedOperationException(
"Create the offer via POST /realms/" + REALM
+ "/protocol/oid4vc/create-credential-offer once offers are no longer bound to the creating client");
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidJwtProofSignatureTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-jwt-proof-signature",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.VciConformanceRealmUtil;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import com.fasterxml.jackson.databind.JsonNode;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidKeyAttestationSignatureTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = IssuerKeyAttestationTest.KeyAttestationRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-key-attestation-signature",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
// Non-HAIP does not use client attestation, but this test exercises key attestation on the credential,
// so the suite still needs the key attestation JWKS advertised (client attestation stays disabled).
@Override
protected JsonNode keyAttestationJwks() {
return VciConformanceRealmUtil.attesterJwks();
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerInvalidNonceTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-invalid-nonce",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.models.oid4vci.CredentialScopeModel;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.tests.conformance.vci.VciConformanceRealmUtil;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import com.fasterxml.jackson.databind.JsonNode;
import static org.keycloak.tests.conformance.vci.VciConformanceRealmUtil.SD_JWT_SCOPE;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
/**
* Issues a credential whose configuration requires key attestations, so the suite includes a valid key attestation
* that Keycloak must accept. The attestation x5c chain is trusted through the {@code conformance-attester-x509}
* trust-material identity provider configured by {@link NonHaipVciConformanceRealmConfig}.
*/
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerKeyAttestationTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = KeyAttestationRequiredRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-happy-flow",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
// Non-HAIP does not use client attestation, but this test requires a key attestation on the credential,
// so the suite still needs the key attestation JWKS advertised (client attestation stays disabled).
@Override
protected JsonNode keyAttestationJwks() {
return VciConformanceRealmUtil.attesterJwks();
}
public static class KeyAttestationRequiredRealmConfig extends NonHaipVciConformanceRealmConfig {
@Override
public RealmBuilder configure(RealmBuilder realm) {
return super.configure(realm).update(rep -> rep.getClientScopes().stream()
.filter(scope -> SD_JWT_SCOPE.equals(scope.getName()))
.forEach(scope -> scope.getAttributes().put(CredentialScopeModel.VC_KEY_ATTESTATION_REQUIRED, "true")));
}
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerMissingProofTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-missing-proof",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerSignedMetadataTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-metadata-test-signed",
ConformanceResult.PASSED,
BrowserInteraction.NONE);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnknownCredentialConfigurationTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-unknown-credential-configuration",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnknownCredentialIdentifierTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-fail-unknown-credential-identifier",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -0,0 +1,49 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnsignedMetadataTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
walletInitiated(),
"oid4vci-1_0-issuer-metadata-test",
ConformanceResult.PASSED,
BrowserInteraction.NONE);
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2026 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.tests.conformance.vci.nonhaip.issuer;
import java.util.stream.Stream;
import org.keycloak.testframework.annotations.InjectRealm;
import org.keycloak.testframework.annotations.KeycloakIntegrationTest;
import org.keycloak.testframework.conformance.runner.BrowserInteraction;
import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import org.keycloak.tests.conformance.vci.nonhaip.AbstractNonHaipVciConformanceTest;
import org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig;
import static org.keycloak.tests.conformance.vci.nonhaip.NonHaipVciConformanceRealmConfig.NON_HAIP_PLAN;
@KeycloakIntegrationTest(config = NonHaipVciConformanceRealmConfig.ServerConfig.class)
public class IssuerUnsupportedEncryptionAlgorithmTest extends AbstractNonHaipVciConformanceTest {
@InjectRealm(config = NonHaipVciConformanceRealmConfig.class, lifecycle = LifeCycle.METHOD)
ManagedRealm realm;
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
NON_HAIP_PLAN,
// This module only runs when a credential response encryption is requested; the generic non-HAIP
// plan does not pin encryption (defaults to plain), so request the encrypted variant explicitly.
encrypted(walletInitiated()),
"oid4vci-1_0-issuer-fail-unsupported-encryption-algorithm",
ConformanceResult.PASSED,
BrowserInteraction.LOGIN);
}
}

View File

@@ -55,7 +55,7 @@ abstract class AbstractVpConformanceTest extends AbstractConformanceTest {
protected Consumer<ModuleRun> interaction(ConformanceModuleVariant moduleVariant) {
return moduleRun -> {
KeycloakVerifierBrowser browser =
new KeycloakVerifierBrowser(suite, keycloakUrls.getBase(), browserSslContext());
new KeycloakVerifierBrowser(server, keycloakUrls.getBase(), browserSslContext());
if (crossDevice()) {
browser.loginCrossDevice(VpConformanceRealmConfig.REALM, VpConformanceRealmConfig.CLIENT_ID,
VpConformanceRealmConfig.IDP_ALIAS, moduleRun);
@@ -77,7 +77,7 @@ abstract class AbstractVpConformanceTest extends AbstractConformanceTest {
try {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
trustStore.load(null, null);
trustStore.setCertificateEntry("conformance-suite", suite.nginxCertificate());
trustStore.setCertificateEntry("conformance-suite", server.nginxCertificate());
KeyStore serverKeyStore = KeyStore.getInstance(certificates.getKeystoreFormat().name());
try (InputStream in = Files.newInputStream(Path.of(certificates.getServerKeyStorePath()))) {

View File

@@ -36,7 +36,7 @@ import org.keycloak.broker.oid4vp.OID4VPIdentityProviderEndpoint;
import org.keycloak.common.util.MultivaluedHashMap;
import org.keycloak.common.util.UriUtils;
import org.keycloak.protocol.oidc.utils.PkceUtils;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.conformance.runner.ModuleRun;
import org.keycloak.util.JsonSerialization;
@@ -57,11 +57,11 @@ final class KeycloakVerifierBrowser {
private static final Duration STATUS_POLL_TIMEOUT = Duration.ofSeconds(30);
private static final Duration STATUS_POLL_INTERVAL = Duration.ofMillis(500);
private final OpenIdConformanceSuite suite;
private final OpenIdConformanceServer suite;
private final String keycloakLocalBaseUrl;
private final HttpClient httpClient;
KeycloakVerifierBrowser(OpenIdConformanceSuite suite, String keycloakLocalBaseUrl, SSLContext sslContext) {
KeycloakVerifierBrowser(OpenIdConformanceServer suite, String keycloakLocalBaseUrl, SSLContext sslContext) {
this.suite = suite;
this.keycloakLocalBaseUrl = keycloakLocalBaseUrl;
this.httpClient = HttpClient.newBuilder()
@@ -93,7 +93,7 @@ final class KeycloakVerifierBrowser {
+ "?client_id=" + urlEncode(clientId)
+ "&response_type=code"
+ "&scope=openid"
+ "&redirect_uri=" + urlEncode(OpenIdConformanceSuite.KEYCLOAK_BASE_URI + "/callback")
+ "&redirect_uri=" + urlEncode(OpenIdConformanceServer.KEYCLOAK_BASE_URI + "/callback")
+ "&code_challenge=" + urlEncode(PkceUtils.generateS256CodeChallenge(codeVerifier))
+ "&code_challenge_method=S256"
+ "&kc_idp_hint=" + urlEncode(idpAlias);
@@ -209,7 +209,7 @@ final class KeycloakVerifierBrowser {
}
private URI rewriteToHostReachable(URI uri) {
if (OpenIdConformanceSuite.KEYCLOAK_BASE_URI.getHost().equals(uri.getHost())) {
if (OpenIdConformanceServer.KEYCLOAK_BASE_URI.getHost().equals(uri.getHost())) {
return URI.create(keycloakLocalBaseUrl + uri.getRawPath()
+ (uri.getRawQuery() != null ? "?" + uri.getRawQuery() : ""));
}

View File

@@ -28,6 +28,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier accepts a valid cross device SD-JWT VC presentation. The wallet uses the QR code link,
* receives an empty JSON object from direct_post, and the browser finishes the login through the
@@ -47,7 +49,7 @@ public class VerifierCrossDeviceHappyFlowTest extends AbstractVpConformanceTest
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT carrying the wrong nonce, presented as an encrypted
* {@code direct_post.jwt} response.
@@ -39,7 +41,7 @@ public class VerifierEncryptedInvalidKbJwtNonceTest extends AbstractVpConformanc
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -28,6 +28,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier accepts a valid same device SD-JWT VC presentation returned as an encrypted
* {@code direct_post.jwt} response.
@@ -41,7 +43,7 @@ public class VerifierEncryptedSameDeviceHappyFlowTest extends AbstractVpConforma
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a credential with an invalid issuer signature.
*/
@@ -38,7 +40,7 @@ public class VerifierInvalidCredentialSignatureTest extends AbstractVpConformanc
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT carrying the wrong audience.
*/
@@ -38,7 +40,7 @@ public class VerifierInvalidKbJwtAudTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT carrying the wrong nonce.
*/
@@ -38,7 +40,7 @@ public class VerifierInvalidKbJwtNonceTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT with an invalid signature.
*/
@@ -38,7 +40,7 @@ public class VerifierInvalidKbJwtSignatureTest extends AbstractVpConformanceTest
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT whose sd_hash does not match the presentation.
*/
@@ -38,7 +40,7 @@ public class VerifierInvalidSdHashTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT issued in the future.
*/
@@ -38,7 +40,7 @@ public class VerifierKbJwtIatInFutureTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -26,6 +26,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceModuleVariant;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier rejects a key binding JWT issued too far in the past.
*/
@@ -38,7 +40,7 @@ public class VerifierKbJwtIatInPastTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -28,6 +28,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier accepts a presentation whose credential carries a minimal holder cnf jwk.
*/
@@ -40,7 +42,7 @@ public class VerifierMinimalCnfJwkTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -30,6 +30,8 @@ import org.keycloak.testframework.realm.ManagedRealm;
import org.junit.jupiter.api.Disabled;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier serves the authorization request object when the wallet retrieves request_uri through
* POST.
@@ -46,7 +48,7 @@ public class VerifierRequestUriMethodPostTest extends AbstractVpConformanceTest
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -28,6 +28,8 @@ import org.keycloak.testframework.conformance.runner.ConformanceResult;
import org.keycloak.testframework.injection.LifeCycle;
import org.keycloak.testframework.realm.ManagedRealm;
import static org.keycloak.tests.conformance.vp.VpConformanceRealmConfig.TEST_PLAN;
/**
* The verifier accepts a valid same device SD-JWT VC presentation.
*/
@@ -40,7 +42,7 @@ public class VerifierSameDeviceHappyFlowTest extends AbstractVpConformanceTest {
@Override
protected Stream<ConformanceModuleVariant> moduleVariants() {
return discoverModuleVariants(
"oid4vp-1final-verifier-test-plan",
TEST_PLAN,
Map.of(
"vp_profile", "plain_vp",
"credential_format", "sd_jwt_vc",

View File

@@ -34,7 +34,7 @@ import org.keycloak.keys.JavaKeystoreKeyProviderFactory;
import org.keycloak.keys.KeyProvider;
import org.keycloak.representations.idm.ComponentExportRepresentation;
import org.keycloak.representations.idm.IdentityProviderRepresentation;
import org.keycloak.testframework.conformance.OpenIdConformanceSuite;
import org.keycloak.testframework.conformance.OpenIdConformanceServer;
import org.keycloak.testframework.realm.ClientBuilder;
import org.keycloak.testframework.realm.RealmBuilder;
import org.keycloak.testframework.realm.RealmConfig;
@@ -44,6 +44,7 @@ import org.keycloak.tests.conformance.ConformanceSigningKey;
public class VpConformanceRealmConfig implements RealmConfig {
public static final String TEST_PLAN = "oid4vp-1final-verifier-test-plan";
public static final String REALM = "oid4vp-verifier";
public static final String CLIENT_ID = "wallet-mock";
public static final String IDP_ALIAS = "oid4vp";
@@ -134,7 +135,7 @@ public class VpConformanceRealmConfig implements RealmConfig {
@Override
public KeycloakServerConfigBuilder configure(KeycloakServerConfigBuilder config) {
return config.features(Profile.Feature.OID4VC_VP)
.option("hostname", OpenIdConformanceSuite.KEYCLOAK_BASE_URI.toString())
.option("hostname", OpenIdConformanceServer.KEYCLOAK_BASE_URI.toString())
.spiOption("keys", "java-keystore", "keystores-path", ConformanceSigningKey.keystoresBaseDir());
}
}