Merge pull request #2025 from thomasdarimont/issue/KEYCLOAK-2311-white-list-role-for-conditional-otp
KEYCLOAK-2311 - Allow to specify role to skip conditional OTP authentication.
This commit is contained in:
commit
0bad78c53b
2 changed files with 42 additions and 20 deletions
|
@ -37,8 +37,9 @@ import static org.keycloak.models.utils.KeycloakModelUtils.hasRole;
|
||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
* <h2>Role</h2>
|
* <h2>Role</h2>
|
||||||
* A role can be used to control the OTP authentication. If the user has the specified role the OTP authentication is forced.
|
* A role can be used to control the OTP authentication. If the user has the specified skip OTP role then OTP authentication is skipped for the user.
|
||||||
* Otherwise if no role is selected the setting is ignored.
|
* If the user has the specified force OTP role, then the OTP authentication is required for the user.
|
||||||
|
* If not configured, e.g. if no role is selected, then this setting is ignored.
|
||||||
* <p>
|
* <p>
|
||||||
* </p>
|
* </p>
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -67,9 +68,11 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
|
|
||||||
public static final String OTP_CONTROL_USER_ATTRIBUTE = "otpControlAttribute";
|
public static final String OTP_CONTROL_USER_ATTRIBUTE = "otpControlAttribute";
|
||||||
|
|
||||||
|
public static final String SKIP_OTP_ROLE = "skipOtpRole";
|
||||||
|
|
||||||
public static final String FORCE_OTP_ROLE = "forceOtpRole";
|
public static final String FORCE_OTP_ROLE = "forceOtpRole";
|
||||||
|
|
||||||
public static final String NO_OTP_REQUIRED_FOR_HTTP_HEADER = "noOtpRequiredForHeaderPattern";
|
public static final String SKIP_OTP_FOR_HTTP_HEADER = "noOtpRequiredForHeaderPattern";
|
||||||
|
|
||||||
public static final String FORCE_OTP_FOR_HTTP_HEADER = "forceOtpForHeaderPattern";
|
public static final String FORCE_OTP_FOR_HTTP_HEADER = "forceOtpForHeaderPattern";
|
||||||
|
|
||||||
|
@ -88,7 +91,7 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryConcludeBasedOn(voteForUserForceOtpRole(context, config), context)) {
|
if (tryConcludeBasedOn(voteForUserRole(context, config), context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -96,14 +99,14 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tryConcludeBasedOn(voteForDefaultFallback(context, config), context)) {
|
if (tryConcludeBasedOn(voteForDefaultFallback(config), context)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
showOtpForm(context);
|
showOtpForm(context);
|
||||||
}
|
}
|
||||||
|
|
||||||
private OtpDecision voteForDefaultFallback(AuthenticationFlowContext context, Map<String, String> config) {
|
private OtpDecision voteForDefaultFallback(Map<String, String> config) {
|
||||||
|
|
||||||
if (!config.containsKey(DEFAULT_OTP_OUTCOME)) {
|
if (!config.containsKey(DEFAULT_OTP_OUTCOME)) {
|
||||||
return ABSTAIN;
|
return ABSTAIN;
|
||||||
|
@ -171,14 +174,14 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
|
|
||||||
private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) {
|
private OtpDecision voteForHttpHeaderMatchesPattern(AuthenticationFlowContext context, Map<String, String> config) {
|
||||||
|
|
||||||
if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(NO_OTP_REQUIRED_FOR_HTTP_HEADER)) {
|
if (!config.containsKey(FORCE_OTP_FOR_HTTP_HEADER) && !config.containsKey(SKIP_OTP_FOR_HTTP_HEADER)) {
|
||||||
return ABSTAIN;
|
return ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
|
MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
|
||||||
|
|
||||||
//Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5)
|
//Inverted to allow white-lists, e.g. for specifying trusted remote hosts: X-Forwarded-Host: (1.2.3.4|1.2.3.5)
|
||||||
if (containsMatchingRequestHeader(requestHeaders, config.get(NO_OTP_REQUIRED_FOR_HTTP_HEADER))) {
|
if (containsMatchingRequestHeader(requestHeaders, config.get(SKIP_OTP_FOR_HTTP_HEADER))) {
|
||||||
return SKIP_OTP;
|
return SKIP_OTP;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -216,19 +219,32 @@ public class ConditionalOtpFormAuthenticator extends OTPFormAuthenticator {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private OtpDecision voteForUserForceOtpRole(AuthenticationFlowContext context, Map<String, String> config) {
|
private OtpDecision voteForUserRole(AuthenticationFlowContext context, Map<String, String> config) {
|
||||||
|
|
||||||
if (!config.containsKey(FORCE_OTP_ROLE)) {
|
if (!config.containsKey(SKIP_OTP_ROLE) && !config.containsKey(FORCE_OTP_ROLE)) {
|
||||||
return ABSTAIN;
|
return ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
RoleModel forceOtpRole = getRoleFromString(context.getRealm(), config.get(FORCE_OTP_ROLE));
|
if (userHasRole(context, config.get(SKIP_OTP_ROLE))) {
|
||||||
UserModel user = context.getUser();
|
return SKIP_OTP;
|
||||||
|
}
|
||||||
|
|
||||||
if (hasRole(user.getRoleMappings(), forceOtpRole)) {
|
if (userHasRole(context, config.get(FORCE_OTP_ROLE))) {
|
||||||
return SHOW_OTP;
|
return SHOW_OTP;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ABSTAIN;
|
return ABSTAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean userHasRole(AuthenticationFlowContext context, String roleName) {
|
||||||
|
|
||||||
|
if (roleName == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
RoleModel role = getRoleFromString(context.getRealm(), roleName);
|
||||||
|
UserModel user = context.getUser();
|
||||||
|
|
||||||
|
return hasRole(user.getRoleMappings(), role);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,20 +98,26 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
|
||||||
"If attribute value is 'force' then OTP is always required. " +
|
"If attribute value is 'force' then OTP is always required. " +
|
||||||
"If value is 'skip' the OTP auth is skipped. Otherwise this check is ignored.");
|
"If value is 'skip' the OTP auth is skipped. Otherwise this check is ignored.");
|
||||||
|
|
||||||
|
ProviderConfigProperty skipOtpRole = new ProviderConfigProperty();
|
||||||
|
skipOtpRole.setType(ROLE_TYPE);
|
||||||
|
skipOtpRole.setName(SKIP_OTP_ROLE);
|
||||||
|
skipOtpRole.setLabel("Skip OTP for Role");
|
||||||
|
skipOtpRole.setHelpText("OTP is always skipped if user has the given Role.");
|
||||||
|
|
||||||
ProviderConfigProperty forceOtpRole = new ProviderConfigProperty();
|
ProviderConfigProperty forceOtpRole = new ProviderConfigProperty();
|
||||||
forceOtpRole.setType(ROLE_TYPE);
|
forceOtpRole.setType(ROLE_TYPE);
|
||||||
forceOtpRole.setName(FORCE_OTP_ROLE);
|
forceOtpRole.setName(FORCE_OTP_ROLE);
|
||||||
forceOtpRole.setLabel("Force OTP for Role");
|
forceOtpRole.setLabel("Force OTP for Role");
|
||||||
forceOtpRole.setHelpText("OTP is always required if user has the given Role.");
|
forceOtpRole.setHelpText("OTP is always required if user has the given Role.");
|
||||||
|
|
||||||
ProviderConfigProperty noOtpRequiredForHttpHeader = new ProviderConfigProperty();
|
ProviderConfigProperty skipOtpForHttpHeader = new ProviderConfigProperty();
|
||||||
noOtpRequiredForHttpHeader.setType(STRING_TYPE);
|
skipOtpForHttpHeader.setType(STRING_TYPE);
|
||||||
noOtpRequiredForHttpHeader.setName(NO_OTP_REQUIRED_FOR_HTTP_HEADER);
|
skipOtpForHttpHeader.setName(SKIP_OTP_FOR_HTTP_HEADER);
|
||||||
noOtpRequiredForHttpHeader.setLabel("No OTP for Header");
|
skipOtpForHttpHeader.setLabel("Skip OTP for Header");
|
||||||
noOtpRequiredForHttpHeader.setHelpText("OTP required if a HTTP request header does not match the given pattern." +
|
skipOtpForHttpHeader.setHelpText("OTP is skipped if a HTTP request header does matches the given pattern." +
|
||||||
"Can be used to specify trusted networks via: X-Forwarded-Host: (1.2.3.4|1.2.3.5)." +
|
"Can be used to specify trusted networks via: X-Forwarded-Host: (1.2.3.4|1.2.3.5)." +
|
||||||
"In this case requests from 1.2.3.4 and 1.2.3.5 come from a trusted source.");
|
"In this case requests from 1.2.3.4 and 1.2.3.5 come from a trusted source.");
|
||||||
noOtpRequiredForHttpHeader.setDefaultValue("");
|
skipOtpForHttpHeader.setDefaultValue("");
|
||||||
|
|
||||||
ProviderConfigProperty forceOtpForHttpHeader = new ProviderConfigProperty();
|
ProviderConfigProperty forceOtpForHttpHeader = new ProviderConfigProperty();
|
||||||
forceOtpForHttpHeader.setType(STRING_TYPE);
|
forceOtpForHttpHeader.setType(STRING_TYPE);
|
||||||
|
@ -127,6 +133,6 @@ public class ConditionalOtpFormAuthenticatorFactory implements AuthenticatorFact
|
||||||
defaultOutcome.setDefaultValue(asList(SKIP, FORCE));
|
defaultOutcome.setDefaultValue(asList(SKIP, FORCE));
|
||||||
defaultOutcome.setHelpText("What to do in case of every check abstains. Defaults to force OTP authentication.");
|
defaultOutcome.setHelpText("What to do in case of every check abstains. Defaults to force OTP authentication.");
|
||||||
|
|
||||||
return asList(forceOtpUserAttribute, forceOtpRole, noOtpRequiredForHttpHeader, forceOtpForHttpHeader, defaultOutcome);
|
return asList(forceOtpUserAttribute, skipOtpRole, forceOtpRole, skipOtpForHttpHeader, forceOtpForHttpHeader, defaultOutcome);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue