Created
January 8, 2016 16:02
-
-
Save thomasdarimont/933c73ca0332299d0414 to your computer and use it in GitHub Desktop.
Dynamic OTP Validation support for Keycloak 1.7.x
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:[email protected]">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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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:[email protected]">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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
org.keycloak.authentication.authenticators.browser.DynamicOtpFormAuthenticatorFactory |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment