Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public final class Oid4VciConstants {

public static final String CREDENTIAL_SUBJECT = "credentialSubject";

public static final String BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE = "batch_credential_issuance.batch_size";

private Oid4VciConstants() {
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,29 @@ private static String getDeferredCredentialEndpoint(KeycloakContext context) {
}

private CredentialIssuer.BatchCredentialIssuance getBatchCredentialIssuance(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();
String batchSize = realm.getAttribute("batch_credential_issuance.batch_size");
return getBatchCredentialIssuance(session.getContext().getRealm());
}

/**
* Returns the batch credential issuance configuration for the given realm.
* This method is public and static to facilitate testing without requiring session state management.
*
* @param realm The realm model
* @return The batch credential issuance configuration or null if not configured or invalid
*/
public static CredentialIssuer.BatchCredentialIssuance getBatchCredentialIssuance(RealmModel realm) {
String batchSize = realm.getAttribute(Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE);
if (batchSize != null) {
try {
int parsedBatchSize = Integer.parseInt(batchSize);
if (parsedBatchSize < 2) {
LOGGER.warnf("%s must be 2 or greater, but was %d. Skipping batch_credential_issuance.", Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE, parsedBatchSize);
return null;
}
return new CredentialIssuer.BatchCredentialIssuance()
.setBatchSize(Integer.parseInt(batchSize));
.setBatchSize(parsedBatchSize);
} catch (Exception e) {
LOGGER.warnf(e, "Failed to parse batch_credential_issuance.batch_size from realm attributes.");
LOGGER.warnf(e, "Failed to parse %s from realm attributes.", Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE);
}
}
return null;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP;
import static org.keycloak.jose.jwe.JWEConstants.RSA_OAEP_256;
import static org.keycloak.protocol.oid4vc.issuance.OID4VCIssuerWellKnownProvider.ATTR_ENCRYPTION_REQUIRED;
import org.keycloak.constants.Oid4VciConstants;


public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest {
Expand All @@ -83,7 +84,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
public void configureTestRealm(RealmRepresentation testRealm) {
Map<String, String> attributes = Optional.ofNullable(testRealm.getAttributes()).orElseGet(HashMap::new);
attributes.put("credential_response_encryption.encryption_required", "true");
attributes.put("batch_credential_issuance.batch_size", "10");
attributes.put(Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE, "10");
attributes.put("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");
attributes.put(ATTR_ENCRYPTION_REQUIRED, "true");
testRealm.setAttributes(attributes);
Expand Down Expand Up @@ -190,7 +191,7 @@ private static CredentialIssuer getCredentialIssuer(KeycloakSession session) {
RealmModel realm = session.getContext().getRealm();

realm.setAttribute(ATTR_ENCRYPTION_REQUIRED, "true");
realm.setAttribute("batch_credential_issuance.batch_size", "10");
realm.setAttribute(Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE, "10");
realm.setAttribute("signed_metadata", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.XYZ123abc");

OID4VCIssuerWellKnownProvider provider = new OID4VCIssuerWellKnownProvider(session);
Expand Down Expand Up @@ -413,6 +414,57 @@ public static void testCredentialConfig(SuiteContext suiteContext, KeycloakTesti
}));
}

@Test
public void testBatchCredentialIssuanceValidation() {
KeycloakTestingClient testingClient = this.testingClient;

// Valid batch size (2 or greater) should be accepted
testBatchSizeValidation(testingClient, "5", true, 5);

// Invalid batch size (less than 2) should be rejected
testBatchSizeValidation(testingClient, "1", false, null);

// Edge case - batch size exactly 2 should be accepted
testBatchSizeValidation(testingClient, "2", true, 2);

// Zero batch size should be rejected
testBatchSizeValidation(testingClient, "0", false, null);

// Negative batch size should be rejected
testBatchSizeValidation(testingClient, "-1", false, null);

// Large valid batch size should be accepted
testBatchSizeValidation(testingClient, "1000", true, 1000);

// Non-numeric value should be rejected (parsing exception)
testBatchSizeValidation(testingClient, "invalid", false, null);
}

private void testBatchSizeValidation(KeycloakTestingClient testingClient, String batchSize, boolean shouldBePresent, Integer expectedValue) {
testingClient
.server(TEST_REALM_NAME)
.run(session -> {
// Create a new isolated realm for testing
RealmModel testRealm = session.realms().createRealm("test-batch-validation-" + batchSize);

try {
testRealm.setAttribute(Oid4VciConstants.BATCH_CREDENTIAL_ISSUANCE_BATCH_SIZE, batchSize);

CredentialIssuer.BatchCredentialIssuance result = OID4VCIssuerWellKnownProvider.getBatchCredentialIssuance(testRealm);

if (shouldBePresent) {
Assert.assertNotNull("batch_credential_issuance should be present for batch size " + batchSize, result);
Assert.assertEquals("batch_credential_issuance should have correct batch size for " + batchSize,
expectedValue, result.getBatchSize());
} else {
Assert.assertNull("batch_credential_issuance should be null for invalid batch size " + batchSize, result);
}
} finally {
session.realms().removeRealm(testRealm.getId());
}
});
}

public static void extendConfigureTestRealm(RealmRepresentation testRealm, ClientRepresentation clientRepresentation) {
if (testRealm.getComponents() == null) {
testRealm.setComponents(new MultivaluedHashMap<>());
Expand Down