Fix NPE in ConditionalOtpFormAuthenticator if no configuration
Closes #34298 Signed-off-by: rmartinc <rmartinc@redhat.com>
This commit is contained in:
parent
a79b67cac8
commit
78aa08941a
2 changed files with 59 additions and 33 deletions
|
@ -18,16 +18,19 @@
|
|||
package org.keycloak.authentication.authenticators.browser;
|
||||
|
||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||
import org.keycloak.models.AuthenticatorConfigModel;
|
||||
import org.keycloak.models.KeycloakSession;
|
||||
import org.keycloak.models.RealmModel;
|
||||
import org.keycloak.models.RoleModel;
|
||||
import org.keycloak.models.UserModel;
|
||||
|
||||
import jakarta.ws.rs.core.MultivaluedMap;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.ABSTAIN;
|
||||
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.SHOW_OTP;
|
||||
|
@ -105,8 +108,8 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
|
||||
@Override
|
||||
public void authenticate(AuthenticationFlowContext context) {
|
||||
|
||||
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
||||
AuthenticatorConfigModel model = context.getAuthenticatorConfig();
|
||||
Map<String, String> config = model != null? model.getConfig() : Collections.emptyMap();
|
||||
|
||||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context.getUser(), config), context)) {
|
||||
return;
|
||||
|
@ -284,30 +287,32 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
|||
|
||||
private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||
MultivaluedMap<String, String> requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders();
|
||||
return realm.getAuthenticatorConfigsStream().anyMatch(configModel -> {
|
||||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, configModel.getConfig()))) {
|
||||
List<Map<String,String>> configs = realm.getAuthenticatorConfigsStream().map(AuthenticatorConfigModel::getConfig)
|
||||
.filter(this::containsConditionalOtpConfig)
|
||||
.collect(Collectors.toList());
|
||||
if (configs.isEmpty()) {
|
||||
// no configuration at all means it is configured
|
||||
return true;
|
||||
}
|
||||
return configs.stream().anyMatch(config -> {
|
||||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, config))) {
|
||||
return true;
|
||||
}
|
||||
if (tryConcludeBasedOn(voteForUserRole(realm, user, configModel.getConfig()))) {
|
||||
if (tryConcludeBasedOn(voteForUserRole(realm, user, config))) {
|
||||
return true;
|
||||
}
|
||||
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()))) {
|
||||
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, config))) {
|
||||
return true;
|
||||
}
|
||||
if (configModel.getConfig().get(DEFAULT_OTP_OUTCOME) != null
|
||||
&& configModel.getConfig().get(DEFAULT_OTP_OUTCOME).equals(FORCE)
|
||||
&& configModel.getConfig().size() <= 1) {
|
||||
if (config.get(DEFAULT_OTP_OUTCOME) != null
|
||||
&& config.get(DEFAULT_OTP_OUTCOME).equals(FORCE)
|
||||
&& config.size() <= 1) {
|
||||
return true;
|
||||
}
|
||||
if (containsConditionalOtpConfig(configModel.getConfig())
|
||||
&& voteForUserOtpControlAttribute(user, configModel.getConfig()) == ABSTAIN
|
||||
&& voteForUserRole(realm, user, configModel.getConfig()) == ABSTAIN
|
||||
&& voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()) == ABSTAIN
|
||||
&& (voteForDefaultFallback(configModel.getConfig()) == SHOW_OTP
|
||||
|| voteForDefaultFallback(configModel.getConfig()) == ABSTAIN)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
return voteForUserOtpControlAttribute(user, config) == ABSTAIN
|
||||
&& voteForUserRole(realm, user, config) == ABSTAIN
|
||||
&& voteForHttpHeaderMatchesPattern(requestHeaders, config) == ABSTAIN
|
||||
&& (voteForDefaultFallback(config) == SHOW_OTP || voteForDefaultFallback(config) == ABSTAIN);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -439,6 +439,25 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
|||
assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void conditionalOTPEmptyConfiguration() {
|
||||
// prepare config empty
|
||||
setConditionalOTPForm(null);
|
||||
|
||||
// test OTP is required
|
||||
driver.navigate().to(oauth.getLoginFormUrl());
|
||||
testRealmLoginPage.form().login(testUser);
|
||||
|
||||
assertTrue(loginConfigTotpPage.isCurrent());
|
||||
|
||||
configureOTP();
|
||||
driver.navigate().to(oauth.getLoginFormUrl());
|
||||
testRealmLoginPage.form().login(testUser);
|
||||
|
||||
// verify that the page is login page, not totp setup
|
||||
assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
|
||||
}
|
||||
|
||||
private RoleRepresentation getOrCreateOTPRole() {
|
||||
try {
|
||||
return testRealmResource().roles().get("otp_role").toRepresentation();
|
||||
|
@ -526,9 +545,9 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
|||
flow.setTopLevel(true);
|
||||
flow.setBuiltIn(false);
|
||||
|
||||
Response response = getAuthMgmtResource().createFlow(flow);
|
||||
assertEquals(flowAlias + " create success", 201, response.getStatus());
|
||||
response.close();
|
||||
try (Response response = getAuthMgmtResource().createFlow(flow)) {
|
||||
assertEquals(flowAlias + " create success", 201, response.getStatus());
|
||||
}
|
||||
|
||||
//add execution - username-password form
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
|
@ -551,19 +570,21 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
|||
realm.setBrowserFlow(flowAlias);
|
||||
testRealmResource().update(realm);
|
||||
|
||||
//get executionId
|
||||
String executionId = getExecution(flowAlias, provider).getId();
|
||||
if (config != null) {
|
||||
//get executionId
|
||||
String executionId = getExecution(flowAlias, provider).getId();
|
||||
|
||||
//prepare auth config
|
||||
AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
|
||||
authConfig.setAlias("Config alias");
|
||||
authConfig.setConfig(config);
|
||||
//prepare auth config
|
||||
AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
|
||||
authConfig.setAlias("Config alias");
|
||||
authConfig.setConfig(config);
|
||||
|
||||
//add auth config to the execution
|
||||
response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig);
|
||||
assertEquals("new execution success", 201, response.getStatus());
|
||||
getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response));
|
||||
response.close();
|
||||
//add auth config to the execution
|
||||
try (Response response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig)) {
|
||||
assertEquals("new execution success", 201, response.getStatus());
|
||||
getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue