diff --git a/docbook/reference/en/en-US/modules/proxy.xml b/docbook/reference/en/en-US/modules/proxy.xml index 3a17557dd2..b38fbbb646 100755 --- a/docbook/reference/en/en-US/modules/proxy.xml +++ b/docbook/reference/en/en-US/modules/proxy.xml @@ -182,106 +182,162 @@ $ java -jar bin/launcher.jar [your-config.json]
Application Config - - Next under the applications array attribute, you can define one or more applications per host you are proxying. - - - base-path - - - The base context root for the application. Must start with '/' REQUIRED.. - - - - - error-page - - - If the proxy has an error, it will display the target application's error page relative URL OPTIONAL.. - This is a relative path to the base-path. In the example above it would be /customer-portal/error.html. - - - - - adapter-config - - - REQUIRED.. Same configuration as any other keycloak adapter. See Adapter Config - - - - - + + Next under the applications array attribute, you can define one or more applications per host you are proxying. + + + base-path + + + The base context root for the application. Must start with '/' REQUIRED.. + + + + + error-page + + + If the proxy has an error, it will display the target application's error page relative URL OPTIONAL.. + This is a relative path to the base-path. In the example above it would be /customer-portal/error.html. + + + + + adapter-config + + + REQUIRED.. Same configuration as any other keycloak adapter. See Adapter Config + + + + +
Constraint Config - - - Next under each application you can define one or more constraints in the constraints array attribute. - A constraint defines a URL pattern relative to the base-path. You can deny, permit, or require authentication for - a specific URL pattern. You can specify roles allowed for that path as well. More specific constraints will take - precedence over more general ones. + + Next under each application you can define one or more constraints in the constraints array attribute. + A constraint defines a URL pattern relative to the base-path. You can deny, permit, or require authentication for + a specific URL pattern. You can specify roles allowed for that path as well. More specific constraints will take + precedence over more general ones. + + + pattern + + + URL pattern to match relative to the base-path of the application. Must start with '/' REQUIRED.. + You may only have one wildcard and it must come at the end of the pattern. Valid /foo/bar/* and /foo/*.txt + Not valid: /*/foo/*. + + + + + roles-allowed + + + Array of strings of roles allowed to access this url pattern. OPTIONAL.. + + + + + methods + + + Array of strings of HTTP methods that will exclusively match this pattern and HTTP request. OPTIONAL.. + + + + + excluded-methods + + + Array of strings of HTTP methods that will be ignored when match this pattern. OPTIONAL.. + + + + + deny + + + Deny all access to this URL pattern. OPTIONAL.. + + + + + permit + + + Permit all access without requiring authentication or a role mapping. OPTIONAL.. + + + + + permit-and-inject + + + Permit all access, but inject the headers, if user is already authenticated.OPTIONAL.. + + + + + authenticate + + + Require authentication for this pattern, but no role mapping. OPTIONAL.. + + + + + +
+
+
+ Header Names Config + + Next under the list of applications you can override the defaults for the names of the header fields injected by the proxy (see Keycloak Identity Headers). + This mapping is optional. - pattern + keycloak-subject - URL pattern to match relative to the base-path of the application. Must start with '/' REQUIRED.. - You may only have one wildcard and it must come at the end of the pattern. Valid /foo/bar/* and /foo/*.txt - Not valid: /*/foo/*. + e.g. MYAPP_USER_ID - roles-allowed + keycloak-username - Array of strings of roles allowed to access this url pattern. OPTIONAL.. + e.g. MYAPP_USER_NAME - methods + keycloak-email - Array of strings of HTTP methods that will exclusively match this pattern and HTTP request. OPTIONAL.. + e.g. MYAPP_USER_EMAIL - excluded-methods + keycloak-name - Array of strings of HTTP methods that will be ignored when match this pattern. OPTIONAL.. + e.g. MYAPP_USER_ID - deny + keycloak-access-token - Deny all access to this URL pattern. OPTIONAL.. - - - - - permit - - - Permit all access without requiring authentication or a role mapping. OPTIONAL.. - - - - - authenticate - - - Require authentication for this pattern, but no role mapping. OPTIONAL.. + e.g. MYAPP_ACCESS_TOKEN - -
+
@@ -333,6 +389,14 @@ $ java -jar bin/launcher.jar [your-config.json] + Header field names can be configured using a map of header-names in configuration file: +
\ No newline at end of file diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java index 58186dc298..365feea9a6 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintAuthorizationHandler.java @@ -6,43 +6,68 @@ import io.undertow.util.HttpString; import org.keycloak.adapters.undertow.KeycloakUndertowAccount; import org.keycloak.representations.IDToken; +import java.util.HashMap; +import java.util.Map; + /** * @author Bill Burke * @version $Revision: 1 $ */ public class ConstraintAuthorizationHandler implements HttpHandler { - public static final HttpString KEYCLOAK_SUBJECT = new HttpString("KEYCLOAK-SUBJECT"); - public static final HttpString KEYCLOAK_USERNAME = new HttpString("KEYCLOAK-USERNAME"); - public static final HttpString KEYCLOAK_EMAIL = new HttpString("KEYCLOAK-EMAIL"); - public static final HttpString KEYCLOAK_NAME = new HttpString("KEYCLOAK-NAME"); - public static final HttpString KEYCLOAK_ACCESS_TOKEN = new HttpString("KEYCLOAK-ACCESS-TOKEN"); + + public static final String KEYCLOAK_SUBJECT = "KEYCLOAK_SUBJECT"; + public static final String KEYCLOAK_USERNAME = "KEYCLOAK_USERNAME"; + public static final String KEYCLOAK_EMAIL = "KEYCLOAK_EMAIL"; + public static final String KEYCLOAK_NAME = "KEYCLOAK_NAME"; + public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN"; + private final Map httpHeaderNames; protected HttpHandler next; protected String errorPage; protected boolean sendAccessToken; - public ConstraintAuthorizationHandler(HttpHandler next, String errorPage, boolean sendAccessToken) { + public ConstraintAuthorizationHandler(HttpHandler next, String errorPage, boolean sendAccessToken, Map headerNames) { this.next = next; this.errorPage = errorPage; this.sendAccessToken = sendAccessToken; + + this.httpHeaderNames = new HashMap<>(); + this.httpHeaderNames.put(KEYCLOAK_SUBJECT, new HttpString(getOrDefault(headerNames, "keycloak-subject", KEYCLOAK_SUBJECT))); + this.httpHeaderNames.put(KEYCLOAK_SUBJECT, new HttpString(getOrDefault(headerNames, "keycloak-username", KEYCLOAK_USERNAME))); + this.httpHeaderNames.put(KEYCLOAK_EMAIL, new HttpString(getOrDefault(headerNames, "keycloak-email", KEYCLOAK_EMAIL))); + this.httpHeaderNames.put(KEYCLOAK_NAME, new HttpString(getOrDefault(headerNames, "keycloak-name", KEYCLOAK_NAME))); + this.httpHeaderNames.put(KEYCLOAK_ACCESS_TOKEN, new HttpString(getOrDefault(headerNames, "keycloak-access-token", KEYCLOAK_ACCESS_TOKEN))); + } + + private String getOrDefault(Map map, String key, String defaultValue) { + return map.containsKey(key) ? map.get(key) : defaultValue; } @Override public void handleRequest(HttpServerExchange exchange) throws Exception { + KeycloakUndertowAccount account = (KeycloakUndertowAccount)exchange.getSecurityContext().getAuthenticatedAccount(); + SingleConstraintMatch match = exchange.getAttachment(ConstraintMatcherHandler.CONSTRAINT_KEY); if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.AUTHENTICATE)) { authenticatedRequest(account, exchange); return; } + if (match != null) { - for (String role : match.getRequiredRoles()) { - if (account.getRoles().contains(role)) { - authenticatedRequest(account, exchange); - return; + if(SecurityInfo.EmptyRoleSemantic.PERMIT_AND_INJECT_IF_AUTHENTICATED.equals(match.getEmptyRoleSemantic())) { + authenticatedRequest(account, exchange); + return; + } else { + for (String role : match.getRequiredRoles()) { + if (account.getRoles().contains(role)) { + authenticatedRequest(account, exchange); + return; + } } } } + if (errorPage != null) { exchange.setRequestPath(errorPage); exchange.setRelativePath(errorPage); @@ -61,20 +86,20 @@ public class ConstraintAuthorizationHandler implements HttpHandler { IDToken idToken = account.getKeycloakSecurityContext().getToken(); if (idToken == null) return; if (idToken.getSubject() != null) { - exchange.getRequestHeaders().put(KEYCLOAK_SUBJECT, idToken.getSubject()); + exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_SUBJECT), idToken.getSubject()); } if (idToken.getPreferredUsername() != null) { - exchange.getRequestHeaders().put(KEYCLOAK_USERNAME, idToken.getPreferredUsername()); + exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_USERNAME), idToken.getPreferredUsername()); } if (idToken.getEmail() != null) { - exchange.getRequestHeaders().put(KEYCLOAK_EMAIL, idToken.getEmail()); + exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_EMAIL), idToken.getEmail()); } if (idToken.getName() != null) { - exchange.getRequestHeaders().put(KEYCLOAK_NAME, idToken.getName()); + exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_NAME), idToken.getName()); } if (sendAccessToken) { - exchange.getRequestHeaders().put(KEYCLOAK_ACCESS_TOKEN, account.getKeycloakSecurityContext().getTokenString()); + exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_ACCESS_TOKEN), account.getKeycloakSecurityContext().getTokenString()); } } next.handleRequest(exchange); diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java index 998302dea1..31a187cf5e 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ConstraintMatcherHandler.java @@ -1,11 +1,12 @@ package org.keycloak.proxy; -import io.undertow.security.handlers.AuthenticationConstraintHandler; +import io.undertow.security.api.AuthenticationMechanism; import io.undertow.server.HttpHandler; import io.undertow.server.HttpServerExchange; import io.undertow.util.AttachmentKey; import org.jboss.logging.Logger; -import org.keycloak.KeycloakSecurityContext; + +import java.util.List; /** * @author Bill Burke @@ -47,10 +48,42 @@ public class ConstraintMatcherHandler implements HttpHandler { } return; } + + if (match.getRequiredRoles().isEmpty() + && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.PERMIT_AND_INJECT_IF_AUTHENTICATED) { + + boolean successfulAuthenticatedMethodFound = isSuccessfulAuthenticatedMethodFound(exchange); + + if(successfulAuthenticatedMethodFound) { + //in case of authenticated we go for injecting headers + exchange.putAttachment(CONSTRAINT_KEY, match); + securedHandler.handleRequest(exchange); + return; + } else { + //in case of not authenticated we just show the resource + unsecuredHandler.handleRequest(exchange); + return; + } + } + log.debug("found constraint"); exchange.getSecurityContext().setAuthenticationRequired(); exchange.putAttachment(CONSTRAINT_KEY, match); securedHandler.handleRequest(exchange); } + + private boolean isSuccessfulAuthenticatedMethodFound(HttpServerExchange exchange) { + boolean successfulAuthenticatedMethodFound = false; + List authenticationMechanisms = exchange.getSecurityContext().getAuthenticationMechanisms(); + + for (AuthenticationMechanism authenticationMechanism : authenticationMechanisms) { + AuthenticationMechanism.AuthenticationMechanismOutcome authenticationMechanismOutcome = + authenticationMechanism.authenticate(exchange, exchange.getSecurityContext()); + if(authenticationMechanismOutcome.equals(AuthenticationMechanism.AuthenticationMechanismOutcome.AUTHENTICATED)) { + successfulAuthenticatedMethodFound = true; + } + } + return successfulAuthenticatedMethodFound; + } } diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyConfig.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyConfig.java index dd41a05d6a..b211fdeb61 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyConfig.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyConfig.java @@ -3,10 +3,7 @@ package org.keycloak.proxy; import org.codehaus.jackson.annotate.JsonProperty; import org.keycloak.representations.adapters.config.AdapterConfig; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author Bill Burke @@ -41,6 +38,8 @@ public class ProxyConfig { protected boolean sendAccessToken; @JsonProperty("applications") protected List applications = new LinkedList(); + @JsonProperty("header-names") + private Map headerNames = new HashMap<>(); public String getBindAddress() { return bindAddress; @@ -154,6 +153,14 @@ public class ProxyConfig { this.sendAccessToken = sendAccessToken; } + public void setHeaderNames(Map headerNames) { + this.headerNames = headerNames; + } + + public Map getHeaderNames() { + return headerNames; + } + public static class Application { @JsonProperty("base-path") protected String basePath; @@ -212,6 +219,8 @@ public class ProxyConfig { protected boolean permit; @JsonProperty("authenticate") protected boolean authenticate; + @JsonProperty("permit-and-inject") + protected boolean permitAndInject; public String getPattern() { return pattern; @@ -253,6 +262,14 @@ public class ProxyConfig { this.authenticate = authenticate; } + public boolean isPermitAndInject() { + return permitAndInject; + } + + public void setPermitAndInject(boolean permitAndInject) { + this.permitAndInject = permitAndInject; + } + public Set getMethods() { return methods; } diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java index 1223faf245..d689e594cf 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/ProxyServerBuilder.java @@ -51,10 +51,7 @@ import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.cert.Certificate; import java.security.cert.X509Certificate; -import java.util.HashSet; -import java.util.LinkedList; -import java.util.List; -import java.util.Set; +import java.util.*; /** * @author Bill Burke @@ -76,6 +73,8 @@ public class ProxyServerBuilder { protected HttpHandler proxyHandler; protected boolean sendAccessToken; + protected Map headerNameConfig; + public ProxyServerBuilder target(String uri) { SimpleProxyClientProvider provider = null; try { @@ -98,6 +97,12 @@ public class ProxyServerBuilder { this.sendAccessToken = flag; return this; } + + public ProxyServerBuilder headerNameConfig(Map headerNameConfig) { + this.headerNameConfig = headerNameConfig; + return this; + } + public ApplicationBuilder application(AdapterConfig config) { return new ApplicationBuilder(config); } @@ -169,6 +174,11 @@ public class ProxyServerBuilder { return this; } + public ConstraintBuilder injectIfAuthenticated() { + semantic = SecurityInfo.EmptyRoleSemantic.PERMIT_AND_INJECT_IF_AUTHENTICATED; + return this; + } + public ConstraintBuilder excludedMethods(Set excludedMethods) { this.excludedMethods = excludedMethods; return this; @@ -222,7 +232,7 @@ public class ProxyServerBuilder { errorPage = base + "/" + errorPage; } } - handler = new ConstraintAuthorizationHandler(handler, errorPage, sendAccessToken); + handler = new ConstraintAuthorizationHandler(handler, errorPage, sendAccessToken, headerNameConfig); handler = new ProxyAuthenticationCallHandler(handler); handler = new ConstraintMatcherHandler(matches, handler, toWrap, errorPage); final List mechanisms = new LinkedList(); @@ -373,6 +383,7 @@ public class ProxyServerBuilder { if (constraint.isDeny()) constraintBuilder.deny(); if (constraint.isPermit()) constraintBuilder.permit(); if (constraint.isAuthenticate()) constraintBuilder.authenticate(); + if (constraint.isPermitAndInject()) constraintBuilder.injectIfAuthenticated(); constraintBuilder.add(); } } @@ -383,6 +394,7 @@ public class ProxyServerBuilder { public static void initOptions(ProxyConfig config, ProxyServerBuilder builder) { builder.sendAccessToken(config.isSendAccessToken()); + builder.headerNameConfig(config.getHeaderNames()); if (config.getBufferSize() != null) builder.setBufferSize(config.getBufferSize()); if (config.getBuffersPerRegion() != null) builder.setBuffersPerRegion(config.getBuffersPerRegion()); if (config.getIoThreads() != null) builder.setIoThreads(config.getIoThreads()); diff --git a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java index 5d291373ab..f7f98af228 100755 --- a/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java +++ b/proxy/proxy-server/src/main/java/org/keycloak/proxy/SecurityInfo.java @@ -46,8 +46,12 @@ public class SecurityInfo implements Cloneable { /** * Mandate authentication but authorize access as no roles to check against. */ - AUTHENTICATE; + AUTHENTICATE, + /** + * Permit access in any case, but provide authorization info only if authorized. + */ + PERMIT_AND_INJECT_IF_AUTHENTICATED; } private volatile EmptyRoleSemantic emptyRoleSemantic = EmptyRoleSemantic.DENY;