adorsys / adorsys/keycloak-config-cli
Sub-flow import fails when the same authenticator appears twice and only one execution has authenticatorConfig
- Vorherrschende Sprache
- Java
- Sterne
- 1.2k
- Forks
- 200
- PR-Merge-Kennzahlen
- Keine gemergten PRs in 30 T.
Beschreibung
### Current Behavior
Importing a custom top-level flow that contains a sub-flow with two executions of the same authenticator fails if they are not both distinguishable by a unique `authenticatorConfig` alias.
Example (keycloak-adaptive-authn `default-risk-evaluator`) : one execution without config (before-auth / factory defaults), another with a dedicated config (user-known).
Error during Keycloak import :
```log
Unexpected size of execution default-risk-evaluator in flow xyz. Expected: 1. Actual: 2
de.adorsys.keycloak.config.exception.ImportProcessingException: Unexpected size of execution default-risk-evaluator in flow not-first-login. Expected: 1. Actual: 2 at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createExecutionForSubFlow(ExecutionFlowsImportService.java:310)
```
This is not the already-fixed case of two executions of the same provider each with a distinct config alias (#317, #978 / PR #980).
### Expected Behavior
A sub-flow may contain the same authenticator more than once, including:
- two executions with distinct `authenticatorConfig` aliases (✅ already supported)
- one execution without config and another with a config
- two executions without config (disambiguate by creation order / priority)
Import should attach the config to the newly created execution, not require that `searchByAlias` returns exactly one row.
### Steps To Reproduce
```markdown
1. Install a custom authenticator that is valid twice in the same flow (ex : `default-risk-evaluator` from keycloak-adaptive-authn).
Built-in equivalent: two `identity-provider-redirector` in a sub-flow, first without `authenticatorConfig`, second with.
2. Import a realm with a non-top-level flow like:
{
"alias": "login",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": false,
"authenticationExecutions": [
{
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 0
},
{
"authenticator": "auth-username-password-form",
"requirement": "REQUIRED",
"priority": 1
},
{
"authenticatorConfig": "idp-2",
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 2
}
]
}
3. Observe the import fail with Unexpected size of execution … Expected: 1. Actual: 2.
```
### Deployment Method
Docker
### Environment
- Keycloak : 26.7.1
- keycloak-config-cli : latest-26
- Custom SPI : keycloak-adaptive-authn (default-risk-evaluator)
### Relevant configuration (sanitized)
```yaml
{
"enabled": true,
"realm": "test",
"authenticationFlows": [
{
"alias": "my-browser",
"providerId": "basic-flow",
"topLevel": true,
"builtIn": false,
"authenticationExecutions": [
{
"authenticatorFlow": true,
"requirement": "REQUIRED",
"priority": 0,
"flowAlias": "login"
}
]
},
{
"alias": "login",
"providerId": "basic-flow",
"topLevel": false,
"builtIn": false,
"authenticationExecutions": [
{
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 0
},
{
"authenticator": "auth-username-password-form",
"requirement": "REQUIRED",
"priority": 1
},
{
"authenticatorConfig": "idp-2",
"authenticator": "identity-provider-redirector",
"requirement": "ALTERNATIVE",
"priority": 2
}
]
}
],
"authenticatorConfig": [
{
"alias": "idp-2",
"config": {
"defaultProvider": "idp-2"
}
}
]
}
```
### Logs / error output
```shell
2026-08-19T09:44:38.272Z ERROR 1 --- [ main] d.a.k.config.KeycloakConfigRunner : Error during Keycloak import: Unexpected size of execution default-risk-evaluator in flow not-first-login. Expected: 1. Actual: 2
de.adorsys.keycloak.config.exception.ImportProcessingException: Unexpected size of execution default-risk-evaluator in flow not-first-login. Expected: 1. Actual: 2
at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createExecutionForSubFlow(ExecutionFlowsImportService.java:310)
at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createExecutionAndExecutionFlowsForSubFlows(ExecutionFlowsImportService.java:252)
at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createAndConfigureExecutionFlow(ExecutionFlowsImportService.java:115)
at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createExecutionOrExecutionFlow(ExecutionFlowsImportService.java:98)
at de.adorsys.keycloak.config.service.ExecutionFlowsImportService.createExecutionsAndExecutionFlows(ExecutionFlowsImportService.java:77)
at de.adorsys.keycloak.config.service.AuthenticationFlowsImportService.createTopLevelFlow(AuthenticationFlowsImportService.java:207)
at de.adorsys.keycloak.config.service.AuthenticationFlowsImportService.createOrUpdateTopLevelFlow(AuthenticationFlowsImportService.java:196)
at de.adorsys.keycloak.config.service.AuthenticationFlowsImportService.createOrUpdateTopLevelFlows(AuthenticationFlowsImportService.java:176)
at de.adorsys.keycloak.config.service.AuthenticationFlowsImportService.doImport(AuthenticationFlowsImportService.java:109)
at de.adorsys.keycloak.config.service.RealmImportService.configureRealm(RealmImportService.java:248)
at de.adorsys.keycloak.config.service.RealmImportService.createRealm(RealmImportService.java:203)
at de.adorsys.keycloak.config.service.RealmImportService.doImport(RealmImportService.java:165)
at de.adorsys.keycloak.config.KeycloakConfigRunner.run(KeycloakConfigRunner.java:89)
at org.springframework.boot.SpringApplication.lambda$callRunner$5(SpringApplication.java:789)
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82)
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60)
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:797)
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:788)
at org.springframework.boot.SpringApplication.lambda$callRunners$3(SpringApplication.java:773)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(Unknown Source)
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.copyInto(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(Unknown Source)
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(Unknown Source)
at java.base/java.util.stream.AbstractPipeline.evaluate(Unknown Source)
at java.base/java.util.stream.ReferencePipeline.forEach(Unknown Source)
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:773)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:325)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1362)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1351)
at de.adorsys.keycloak.config.KeycloakConfigApplication.main(KeycloakConfigApplication.java:34)
at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:102)
at org.springframework.boot.loader.launch.Launcher.launch(Launcher.java:64)
at org.springframework.boot.loader.launch.PropertiesLauncher.main(PropertiesLauncher.java:580)
2026-08-19T09:44:38.274Z INFO 1 --- [ main] d.a.k.config.KeycloakConfigRunner : keycloak-config-cli ran in 00:04.134.
```
### Anything else?
ExecutionFlowRepository.searchByAlias matches providerId, then :
```
if (authenticationConfig != null && f.getAlias() != null) {
return Objects.equals(f.getAlias(), authenticationConfig);
}
return true; // null alias is a wildcard
createExecutionForSubFlow then keeps executions with authenticationConfig == null and requires size == 1.
```
When the first same-provider execution has no config, its alias stays null. Creating the second execution (config not attached yet) yields two matches.
Workaround : give every duplicate authenticator a unique authenticatorConfig alias (even if the config body is empty / defaults).
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.