adorsys / adorsys/keycloak-config-cli

Sub-flow import fails when the same authenticator appears twice and only one execution has authenticatorConfig

Offen
#1,671 0 Kommentare 0 Reaktionen 0 zugewiesene Personen Auf GitHub ansehen
bug
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

Beitragsleitfaden öffnen

Bewertung

Dieses Issue wurde noch nicht bewertet.

Neue Issues direkt in Ihr Postfach

Eine kurze Übersicht über anfängerfreundliche GitHub-Issues.