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;
|
package org.keycloak.authentication.authenticators.browser;
|
||||||
|
|
||||||
import org.keycloak.authentication.AuthenticationFlowContext;
|
import org.keycloak.authentication.AuthenticationFlowContext;
|
||||||
|
import org.keycloak.models.AuthenticatorConfigModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.RoleModel;
|
import org.keycloak.models.RoleModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
|
|
||||||
import jakarta.ws.rs.core.MultivaluedMap;
|
import jakarta.ws.rs.core.MultivaluedMap;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.regex.Pattern;
|
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.ABSTAIN;
|
||||||
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.SHOW_OTP;
|
import static org.keycloak.authentication.authenticators.browser.ConditionalOtpFormAuthenticator.OtpDecision.SHOW_OTP;
|
||||||
|
@ -105,8 +108,8 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void authenticate(AuthenticationFlowContext context) {
|
public void authenticate(AuthenticationFlowContext context) {
|
||||||
|
AuthenticatorConfigModel model = context.getAuthenticatorConfig();
|
||||||
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
|
Map<String, String> config = model != null? model.getConfig() : Collections.emptyMap();
|
||||||
|
|
||||||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context.getUser(), config), context)) {
|
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(context.getUser(), config), context)) {
|
||||||
return;
|
return;
|
||||||
|
@ -284,30 +287,32 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
|
|
||||||
private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) {
|
private boolean isOTPRequired(KeycloakSession session, RealmModel realm, UserModel user) {
|
||||||
MultivaluedMap<String, String> requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders();
|
MultivaluedMap<String, String> requestHeaders = session.getContext().getRequestHeaders().getRequestHeaders();
|
||||||
return realm.getAuthenticatorConfigsStream().anyMatch(configModel -> {
|
List<Map<String,String>> configs = realm.getAuthenticatorConfigsStream().map(AuthenticatorConfigModel::getConfig)
|
||||||
if (tryConcludeBasedOn(voteForUserOtpControlAttribute(user, configModel.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;
|
return true;
|
||||||
}
|
}
|
||||||
if (tryConcludeBasedOn(voteForUserRole(realm, user, configModel.getConfig()))) {
|
if (tryConcludeBasedOn(voteForUserRole(realm, user, config))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()))) {
|
if (tryConcludeBasedOn(voteForHttpHeaderMatchesPattern(requestHeaders, config))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (configModel.getConfig().get(DEFAULT_OTP_OUTCOME) != null
|
if (config.get(DEFAULT_OTP_OUTCOME) != null
|
||||||
&& configModel.getConfig().get(DEFAULT_OTP_OUTCOME).equals(FORCE)
|
&& config.get(DEFAULT_OTP_OUTCOME).equals(FORCE)
|
||||||
&& configModel.getConfig().size() <= 1) {
|
&& config.size() <= 1) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (containsConditionalOtpConfig(configModel.getConfig())
|
return voteForUserOtpControlAttribute(user, config) == ABSTAIN
|
||||||
&& voteForUserOtpControlAttribute(user, configModel.getConfig()) == ABSTAIN
|
&& voteForUserRole(realm, user, config) == ABSTAIN
|
||||||
&& voteForUserRole(realm, user, configModel.getConfig()) == ABSTAIN
|
&& voteForHttpHeaderMatchesPattern(requestHeaders, config) == ABSTAIN
|
||||||
&& voteForHttpHeaderMatchesPattern(requestHeaders, configModel.getConfig()) == ABSTAIN
|
&& (voteForDefaultFallback(config) == SHOW_OTP || voteForDefaultFallback(config) == ABSTAIN);
|
||||||
&& (voteForDefaultFallback(configModel.getConfig()) == SHOW_OTP
|
|
||||||
|| voteForDefaultFallback(configModel.getConfig()) == ABSTAIN)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -439,6 +439,25 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
||||||
assertCurrentUrlStartsWith(testLoginOneTimeCodePage);
|
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() {
|
private RoleRepresentation getOrCreateOTPRole() {
|
||||||
try {
|
try {
|
||||||
return testRealmResource().roles().get("otp_role").toRepresentation();
|
return testRealmResource().roles().get("otp_role").toRepresentation();
|
||||||
|
@ -526,9 +545,9 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
||||||
flow.setTopLevel(true);
|
flow.setTopLevel(true);
|
||||||
flow.setBuiltIn(false);
|
flow.setBuiltIn(false);
|
||||||
|
|
||||||
Response response = getAuthMgmtResource().createFlow(flow);
|
try (Response response = getAuthMgmtResource().createFlow(flow)) {
|
||||||
assertEquals(flowAlias + " create success", 201, response.getStatus());
|
assertEquals(flowAlias + " create success", 201, response.getStatus());
|
||||||
response.close();
|
}
|
||||||
|
|
||||||
//add execution - username-password form
|
//add execution - username-password form
|
||||||
Map<String, Object> data = new HashMap<>();
|
Map<String, Object> data = new HashMap<>();
|
||||||
|
@ -550,20 +569,22 @@ public class CustomAuthFlowOTPTest extends AbstractCustomAccountManagementTest {
|
||||||
RealmRepresentation realm = testRealmResource().toRepresentation();
|
RealmRepresentation realm = testRealmResource().toRepresentation();
|
||||||
realm.setBrowserFlow(flowAlias);
|
realm.setBrowserFlow(flowAlias);
|
||||||
testRealmResource().update(realm);
|
testRealmResource().update(realm);
|
||||||
|
|
||||||
//get executionId
|
|
||||||
String executionId = getExecution(flowAlias, provider).getId();
|
|
||||||
|
|
||||||
//prepare auth config
|
if (config != null) {
|
||||||
AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
|
//get executionId
|
||||||
authConfig.setAlias("Config alias");
|
String executionId = getExecution(flowAlias, provider).getId();
|
||||||
authConfig.setConfig(config);
|
|
||||||
|
|
||||||
//add auth config to the execution
|
//prepare auth config
|
||||||
response = getAuthMgmtResource().newExecutionConfig(executionId, authConfig);
|
AuthenticatorConfigRepresentation authConfig = new AuthenticatorConfigRepresentation();
|
||||||
assertEquals("new execution success", 201, response.getStatus());
|
authConfig.setAlias("Config alias");
|
||||||
getCleanup().addAuthenticationConfigId(ApiUtil.getCreatedId(response));
|
authConfig.setConfig(config);
|
||||||
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