8000 Add conditional http request header to close #14604 by kreativmonkey · Pull Request #14605 · keycloak/keycloak · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Add conditional http request header to close #14604 #14605

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
< 8000 /tr>
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package org.keycloak.authentication.authenticators.conditional;

import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.models.AuthenticatorConfigModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.utils.KeycloakModelUtils;
import org.jboss.logging.Logger;

import javax.ws.rs.core.MultivaluedMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Pattern;

public class ConditionalHttpHeaderAuthenticator implements ConditionalAuthenticator {

public static final ConditionalHttpHeaderAuthenticator SINGLETON = new ConditionalHttpHeaderAuthenticator();
private static final Logger logger = Logger.getLogger(ConditionalHttpHeaderAuthenticator.class);

public boolean containsMatchingRequestHeader(MultivaluedMap<String, String> requestHeaders, String headerPattern) {
if (headerPattern == null) {
logger.debugv("The metching request header pattern are <null>!");
return false;
}

Pattern pattern = Pattern.compile(headerPattern, Pattern.DOTALL | Pattern.CASE_INSENSITIVE);

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()) {
logger.debugv("Pattern {0} matches header entry {1}", headerPattern, headerEntry);
return true;
}
}
}

return false;
}

@Override
public boolean matchCondition(AuthenticationFlowContext context) {
Map<String, String> config = context.getAuthenticatorConfig().getConfig();
if(config == null){
return false;
}

MultivaluedMap<String, String> requestHeaders = context.getHttpRequest().getHttpHeaders().getRequestHeaders();
String headerPattern = config.get(ConditionalHttpHeaderAuthenticatorFactory.HTTP_HEADER_PATTERN);
boolean negateOutcome = Boolean.parseBoolean(config.get(ConditionalHttpHeaderAuthenticatorFactory.NEGATE_OUTCOME));

return (negateOutcome != containsMatchingRequestHeader(requestHeaders, headerPattern));
}

@Override
public boolean requiresUser() {
return false;
}

@Override
public void action(AuthenticationFlowContext context) {
// Not used
}

@Override
public void setRequiredActions(KeycloakSession session, RealmModel realm, UserModel user) {
// Not used
}

@Override
public void close() {
// Does nothing
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package org.keycloak.authentication.authenticators.conditional;

import org.keycloak.Config.Scope;
import org.keycloak.authentication.Authenticator;
import org.keycloak.authentication.AuthenticatorFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticatorFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalAuthenticator;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.AuthenticationExecutionModel.Requirement;
import org.keycloak.models.KeycloakSessionFactory;
import org.keycloak.provider.ProviderConfigProperty;

import java.util.Arrays;
import java.util.List;

/**
* An {@link ConditionalAuthenticatorFactory} for {@link ConditionalHttpHeaderAuthenticator}s.
*
* @author <a href="mailto:preisner@puzzle-itc.de">Sebastian Preisner</a>
*/
public class ConditionalHttpHeaderAuthenticatorFactory implements ConditionalAuthenticatorFactory {
public static final String PROVIDER_ID = "conditional-http-header";

public static final String HTTP_HEADER_PATTERN = "search_pattern";
public static final String NEGATE_OUTCOME = "negate_outcome";

@Override
public void init(Scope config) {
// no-op
}

@Override
public void postInit(KeycloakSessionFactory factory) {
// no-op
}

@Override
public void close() {
// no-op
}

@Override
public String getId() {
return PROVIDER_ID;
}

@Override
public String getDisplayType() {
return "Condition - request header";
}

@Override
public boolean isConfigurable() {
return true;
}

private static final Requirement[] REQUIREMENT_CHOICES = {
AuthenticationExecutionModel.Requirement.REQUIRED,
AuthenticationExecutionModel.Requirement.DISABLED
};

@Override
public AuthenticationExecutionModel.Requirement[] getRequirementChoices() {
return REQUIREMENT_CHOICES;
}

@Override
public boolean isUserSetupAllowed() {
return false;
}

@Override
public String getHelpText() {
return "Flow is executed olny if HTTP request header matches supplied regular expression.";
}

@Override
public List<ProviderConfigProperty> getConfigProperties() {

ProviderConfigProperty HttpHeaderPattern = new ProviderConfigProperty();
HttpHeaderPattern.setType(ProviderConfigProperty.STRING_TYPE);
HttpHeaderPattern.setName(HTTP_HEADER_PATTERN);
HttpHeaderPattern.setLabel("HTTP Header Pattern");
HttpHeaderPattern.setHelpText("If a HTTP request header matches the given pattern the condition will be true." +
"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.");
HttpHeaderPattern.setDefaultValue("");

ProviderConfigProperty negateOutcome = new ProviderConfigProperty();
negateOutcome.setType(ProviderConfigProperty.BOOLEAN_TYPE);
negateOutcome.setName(NEGATE_OUTCOME);
negateOutcome.setLabel("Negate");
negateOutcome.setHelpText("Send false if the given pattern matches.");

return Arrays.asList(HttpHeaderPattern, negateOutcome);
}

@Override
public ConditionalAuthenticator getSingleton() {
return ConditionalHttpHeaderAuthenticator.SINGLETON;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthentica
org.keycloak.authentication.authenticators.conditional.ConditionalUserConfiguredAuthenticatorFactory
org.keycloak.authentication.auth 8000 enticators.conditional.ConditionalLoaAuthenticatorFactory
org.keycloak.authentication.authenticators.conditional.ConditionalUserAttributeValueFactory
org.keycloak.authentication.authenticators.conditional.ConditionalHttpHeaderAuthenticatorFactory
org.keycloak.authentication.authenticators.directgrant.ValidateOTP
org.keycloak.authentication.authenticators.directgrant.ValidatePassword
org.keycloak.authentication.authenticators.directgrant.ValidateUsername
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import org.keycloak.authentication.authenticators.access.DenyAccessAuthenticatorFactory;
import org.keycloak.authentication.authenticators.browser.PasswordFormFactory;
import org.keycloak.authentication.authenticators.browser.UsernameFormFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalHttpHeaderAuthenticatorFactory;
import org.keycloak.authentication.authenticators.conditional.ConditionalRoleAuthenticatorFactory;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
Expand All @@ -26,6 +27,7 @@
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.keycloak.testsuite.forms.BrowserFlowTest.revertFlows;
import static org.keycloak.testsuite.util.ServerURLs.AUTH_SERVER_PORT;

/**
* @author <a href="mailto:mabartos@redhat.com">Martin Bartos</a>
Expand Down Expand Up @@ -293,6 +295,107 @@ public void testSkipOtherExecutionsIfUserHasRoleCondition() {
}
}

/**
* Test httpHeaderCondition with mathing pattern
*/
@Test
public void testDenyAccessWithHttpHeaderConditionMatch() {
String pattern = "Host: localhost:" + AUTH_SERVER_PORT;
boolean negateOutcome = false;
boolean expectedOutcome = false;
denyAccessWithHttpHeader(pattern, negateOutcome, expectedOutcome);
}

/**
* Test httpHeaderCondition with negation and matching pattern
*/
@Test
public void testDenyAccessWithNegateHttpHeaderConditionMatch() {
String pattern = "Host: localhost:" + AUTH_SERVER_PORT;
boolean negateOutcome = true;
boolean expectedOutcome = true;
denyAccessWithHttpHeader(pattern, negateOutcome, expectedOutcome);
}

/**
* Test httpHeaderCondition with no matching pattern
*/
@Test
public void testDenyAccessWithHttpHeaderConditionNoMatch() {
String pattern = "Host: nolocalhost:" + AUTH_SERVER_PORT;
boolean negateOutcome = false;
boolean expectedOutcome = true;
denyAccessWithHttpHeader(pattern, negateOutcome, expectedOutcome);
}

/**
* Test httpHeaderCondition with negation and no matching pattern
*/
@Test
public void testDenyAccessWithNegateHttpHeaderConditionNoMatch() {
String pattern = "Host: nolocalhost:" + AUTH_SERVER_PORT;
boolean negateOutcome = true;
boolean expectedOutcome = false;
denyAccessWithHttpHeader(pattern, negateOutcome, expectedOutcome);
}

/**
* Helper method to configure the Browser flow and call the expected Outcome method
*/
private void denyAccessWithHttpHeader(String httpHeaderPattern, boolean negateOutcome, boolean loginPossible) {
final String flowAlias = "browser - httpHeader condition";
final String errorMessage = "You are at the wrong location";
final String user = "john-doh@localhost";

Map<String, String> attributeConfigMap = new HashMap<>();
attributeConfigMap.put(ConditionalHttpHeaderAuthenticatorFactory.HTTP_HEADER_PATTERN, httpHeaderPattern);
attributeConfigMap.put(ConditionalHttpHeaderAuthenticatorFactory.NEGATE_OUTCOME, Boolean.toString(negateOutcome));

Map<String, String> denyAccessConfigMap = new HashMap<>();
denyAccessConfigMap.put(DenyAccessAuthenticatorFactory.ERROR_MESSAGE, errorMessage);

configureBrowserFlowWithDenyAccessInConditionalFlow(flowAlias, ConditionalHttpHeaderAuthenticatorFactory.PROVIDER_ID ,attributeConfigMap, denyAccessConfigMap);

try{
if (loginPossible) {

final String userId = testRealm().users().search(user).get(0).getId();

loginUsernameOnlyPage.open();
loginUsernameOnlyPage.assertCurrent();
loginUsernameOnlyPage.login(user);

passwordPage.assertCurrent();
passwordPage.login("password");

events.expectLogin()
.user(userId)
.detail(Details.USERNAME, user)
.removeDetail(Details.CONSENT)
.assertEvent();
} else {

loginUsernameOnlyPage.open();
loginUsernameOnlyPage.assertCurrent();
loginUsernameOnlyPage.login(user);

errorPage.assertCurrent();
assertThat(errorPage.getError(), is(errorMessage));

events.expectLogin()
.user((String) null)
.session((String) null)
.error(Errors.ACCESS_DENIED)
.detail(Details.USERNAME, user)
.removeDetail(Details.CONSENT)
.assertEvent();
}
} finally {
revertFlows(testRealm(), flowAlias);
}

}

/**
* This flow contains:
* UsernameForm REQUIRED
Expand Down
0