Enforce batch_size ≥ 2 validation for batch_credential_issuance (#42003)

Closes #41590

Signed-off-by: forkimenjeckayang <forkimenjeckayang@gmail.com>
This commit is contained in:
forkimenjeckayang
2025-09-03 16:15:55 +01:00
committed by GitHub
parent dc6afee14e
commit a74076e8ab
3 changed files with 75 additions and 6 deletions

View File

@@ -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() {
}
}

View File

@@ -91,14 +91,29 @@ public class OID4VCIssuerWellKnownProvider implements WellKnownProvider {
}
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;

View File

@@ -75,6 +75,7 @@ import static org.keycloak.jose.jwe.JWEConstants.A256GCM;
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 {
@@ -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);
@@ -190,7 +191,7 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
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);
@@ -413,6 +414,57 @@ public class OID4VCIssuerWellKnownProviderTest extends OID4VCIssuerEndpointTest
}));
}
@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<>());