Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ecasilla/742b6dac7e241d528a99e1e093e7d3af to your computer and use it in GitHub Desktop.
Save ecasilla/742b6dac7e241d528a99e1e093e7d3af to your computer and use it in GitHub Desktop.
Dynamic OTP Validation support for Keycloak 1.7.x
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.*;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.util.*;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:thomas.darimont@gmail.com">Thomas Darimont</a>
*/
public class DynamicOtpFormAuthenticator extends OTPFormAuthenticator {
@Override
public void authenticate(AuthenticationFlowContext context) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if (config.isEmpty()) {
context.success();
return;
}
if (userHasOtpTriggerRole(context, config)) {
Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse);
return;
}
String positiveTriggerRequestHeaderPattern = config.get(DynamicOtpFormAuthenticatorFactory.POSITIVE_HTTP_HEADER_PATTERN);
if (positiveTriggerRequestHeaderPattern != null && !positiveTriggerRequestHeaderPattern.trim().isEmpty()) {
if (httpRequestMatchesTriggerPattern(context, DynamicOtpFormAuthenticatorFactory.POSITIVE_HTTP_HEADER_PATTERN)) {
Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse);
return;
}
}
String negativeTriggerRequestHeaderPattern = config.get(DynamicOtpFormAuthenticatorFactory.NEGATIVE_HTTP_HEADER_PATTERN);
if (negativeTriggerRequestHeaderPattern != null && !negativeTriggerRequestHeaderPattern.trim().isEmpty()) {
if (!httpRequestMatchesTriggerPattern(context, negativeTriggerRequestHeaderPattern)) {
Response challengeResponse = challenge(context, null);
context.challenge(challengeResponse);
return;
}
}
context.success();
}
private boolean httpRequestMatchesTriggerPattern(AuthenticationFlowContext context, String triggerRequestHeaderPattern) {
MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
Pattern pattern = Pattern.compile(triggerRequestHeaderPattern, Pattern.DOTALL);
for (Map.Entry<String, List<String>> entry : requestHeaders.entrySet()) {
String key = entry.getKey();
for (String value : entry.getValue()) {
String headerEntry = key.trim() + ": " + value.trim();
if (pattern.matcher(headerEntry).matches()) {
return true;
}
}
}
return false;
}
private boolean userHasOtpTriggerRole(AuthenticationFlowContext context, Map<String, String> config) {
String triggerRole = config.get(DynamicOtpFormAuthenticatorFactory.TRIGGER_ROLE);
if (triggerRole != null) {
for (RoleModel role : context.getUser().getRealmRoleMappings()) {
if (role.isComposite()) {
for (RoleModel compositeRole : role.getComposites()) {
if (triggerRole.equals(compositeRole.getName())) {
return true;
}
}
} else {
if (triggerRole.equals(role.getName())) {
return true;
}
}
}
}
return false;
}
}
package org.keycloak.authentication.authenticators.browser;
import org.keycloak.Config;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.models.UserCredentialModel;
import org.keycloak.provider.ProviderConfigProperty;
import java.util.Arrays;
import java.util.List;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class DynamicOtpFormAuthenticatorFactory implements AuthenticatorFactory {
public static final String PROVIDER_ID = "auth-dynamic-otp-form";
public static final DynamicOtpFormAuthenticator SINGLETON = new DynamicOtpFormAuthenticator();
public static final String TRIGGER_ROLE = "triggerRole";
public static final String POSITIVE_HTTP_HEADER_PATTERN = "positivehttpHeaderPattern";
public static final String NEGATIVE_HTTP_HEADER_PATTERN = "negativehttpHeaderPattern";
@Override
public Authenticator create(KeycloakSession session) {
return SINGLETON;
}
@Override
public void init(Config.Scope config) {
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
@Override
public String getReferenceCategory() {
return UserCredentialModel.TOTP;
}
@Override
public boolean isConfigurable() {
return true;
}
@Override
public boolean isUserSetupAllowed() {
return true;
}
public static final AuthenticationExecutionModel.Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.OPTIONAL,
AuthenticationExecutionModel.Requirement.DISABLED};
@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}
@Override
public String getDisplayType() {
return "Dynamic OTP Form";
}
@Override
public String getHelpText() {
return "Validates a OTP on a separate OTP form. Only shown if required.";
}
@Override
public List<ProviderConfigProperty> getConfigProperties() {
ProviderConfigProperty triggerRole = new ProviderConfigProperty();
triggerRole.setType(ProviderConfigProperty.STRING_TYPE);
triggerRole.setName(TRIGGER_ROLE);
triggerRole.setLabel("Force OTP for Role");
triggerRole.setHelpText("OTP required if user has the given Role.");
triggerRole.setDefaultValue("");
ProviderConfigProperty positiveHttpHeaderPattern = new ProviderConfigProperty();
positiveHttpHeaderPattern.setType(ProviderConfigProperty.STRING_TYPE);
positiveHttpHeaderPattern.setName(POSITIVE_HTTP_HEADER_PATTERN);
positiveHttpHeaderPattern.setLabel("Force OTP for Header");
positiveHttpHeaderPattern.setHelpText("OTP required if a http request header matches the given pattern.");
positiveHttpHeaderPattern.setDefaultValue("");
ProviderConfigProperty negativeHttpHeaderPattern = new ProviderConfigProperty();
negativeHttpHeaderPattern.setType(ProviderConfigProperty.STRING_TYPE);
negativeHttpHeaderPattern.setName(NEGATIVE_HTTP_HEADER_PATTERN);
negativeHttpHeaderPattern.setLabel("No OTP for Header");
negativeHttpHeaderPattern.setHelpText("OTP required if a http request header does not match the given pattern.");
negativeHttpHeaderPattern.setDefaultValue("");
return Arrays.asList(triggerRole, positiveHttpHeaderPattern, negativeHttpHeaderPattern);
}
}
org.keycloak.authentication.authenticators.browser.DynamicOtpFormAuthenticatorFactory
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment