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;