Merge pull request #1298 from AOEpeople/KEYCLOAK-1103

KEYCLOAK-1103 proxy configuration for inject header if user is logged in, but do not enforce login
This commit is contained in:
Bill Burke 2015-06-02 08:04:05 -04:00
commit 5680be743c
6 changed files with 248 additions and 93 deletions

View file

@ -214,7 +214,6 @@ $ java -jar bin/launcher.jar [your-config.json]
</para> </para>
<section> <section>
<title>Constraint Config</title> <title>Constraint Config</title>
<para> <para>
Next under each application you can define one or more constraints in the <literal>constraints</literal> array attribute. Next under each application you can define one or more constraints in the <literal>constraints</literal> array attribute.
A constraint defines a URL pattern relative to the base-path. You can deny, permit, or require authentication for A constraint defines a URL pattern relative to the base-path. You can deny, permit, or require authentication for
@ -271,6 +270,14 @@ $ java -jar bin/launcher.jar [your-config.json]
</para> </para>
</listitem> </listitem>
</varlistentry> </varlistentry>
<varlistentry>
<term>permit-and-inject</term>
<listitem>
<para>
Permit all access, but inject the headers, if user is already authenticated.<emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry> <varlistentry>
<term>authenticate</term> <term>authenticate</term>
<listitem> <listitem>
@ -283,6 +290,55 @@ $ java -jar bin/launcher.jar [your-config.json]
</para> </para>
</section> </section>
</section> </section>
<section>
<title>Header Names Config</title>
<para>
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.
<variablelist>
<varlistentry>
<term>keycloak-subject</term>
<listitem>
<para>
e.g. MYAPP_USER_ID
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>keycloak-username</term>
<listitem>
<para>
e.g. MYAPP_USER_NAME
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>keycloak-email</term>
<listitem>
<para>
e.g. MYAPP_USER_EMAIL
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>keycloak-name</term>
<listitem>
<para>
e.g. MYAPP_USER_ID
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>keycloak-access-token</term>
<listitem>
<para>
e.g. MYAPP_ACCESS_TOKEN
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
</section> </section>
<section> <section>
<title>Keycloak Identity Headers</title> <title>Keycloak Identity Headers</title>
@ -333,6 +389,14 @@ $ java -jar bin/launcher.jar [your-config.json]
</listitem> </listitem>
</varlistentry> </varlistentry>
</variablelist> </variablelist>
Header field names can be configured using a map of <literal>header-names</literal> in configuration file:
<programlisting><![CDATA[
{
"header-names" {
"keycloak-subject": "MY_SUBJECT"
}
}
]]></programlisting>
</para> </para>
</section> </section>
</chapter> </chapter>

View file

@ -6,36 +6,59 @@ import io.undertow.util.HttpString;
import org.keycloak.adapters.undertow.KeycloakUndertowAccount; import org.keycloak.adapters.undertow.KeycloakUndertowAccount;
import org.keycloak.representations.IDToken; import org.keycloak.representations.IDToken;
import java.util.HashMap;
import java.util.Map;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $ * @version $Revision: 1 $
*/ */
public class ConstraintAuthorizationHandler implements HttpHandler { 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 String KEYCLOAK_SUBJECT = "KEYCLOAK_SUBJECT";
public static final HttpString KEYCLOAK_EMAIL = new HttpString("KEYCLOAK-EMAIL"); public static final String KEYCLOAK_USERNAME = "KEYCLOAK_USERNAME";
public static final HttpString KEYCLOAK_NAME = new HttpString("KEYCLOAK-NAME"); public static final String KEYCLOAK_EMAIL = "KEYCLOAK_EMAIL";
public static final HttpString KEYCLOAK_ACCESS_TOKEN = new HttpString("KEYCLOAK-ACCESS-TOKEN"); public static final String KEYCLOAK_NAME = "KEYCLOAK_NAME";
public static final String KEYCLOAK_ACCESS_TOKEN = "KEYCLOAK_ACCESS_TOKEN";
private final Map<String, HttpString> httpHeaderNames;
protected HttpHandler next; protected HttpHandler next;
protected String errorPage; protected String errorPage;
protected boolean sendAccessToken; protected boolean sendAccessToken;
public ConstraintAuthorizationHandler(HttpHandler next, String errorPage, boolean sendAccessToken) { public ConstraintAuthorizationHandler(HttpHandler next, String errorPage, boolean sendAccessToken, Map<String, String> headerNames) {
this.next = next; this.next = next;
this.errorPage = errorPage; this.errorPage = errorPage;
this.sendAccessToken = sendAccessToken; 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<String, String> map, String key, String defaultValue) {
return map.containsKey(key) ? map.get(key) : defaultValue;
} }
@Override @Override
public void handleRequest(HttpServerExchange exchange) throws Exception { public void handleRequest(HttpServerExchange exchange) throws Exception {
KeycloakUndertowAccount account = (KeycloakUndertowAccount)exchange.getSecurityContext().getAuthenticatedAccount(); KeycloakUndertowAccount account = (KeycloakUndertowAccount)exchange.getSecurityContext().getAuthenticatedAccount();
SingleConstraintMatch match = exchange.getAttachment(ConstraintMatcherHandler.CONSTRAINT_KEY); SingleConstraintMatch match = exchange.getAttachment(ConstraintMatcherHandler.CONSTRAINT_KEY);
if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.AUTHENTICATE)) { if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.AUTHENTICATE)) {
authenticatedRequest(account, exchange); authenticatedRequest(account, exchange);
return; return;
} }
if (match != null) { if (match != null) {
if(SecurityInfo.EmptyRoleSemantic.PERMIT_AND_INJECT_IF_AUTHENTICATED.equals(match.getEmptyRoleSemantic())) {
authenticatedRequest(account, exchange);
return;
} else {
for (String role : match.getRequiredRoles()) { for (String role : match.getRequiredRoles()) {
if (account.getRoles().contains(role)) { if (account.getRoles().contains(role)) {
authenticatedRequest(account, exchange); authenticatedRequest(account, exchange);
@ -43,6 +66,8 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
} }
} }
} }
}
if (errorPage != null) { if (errorPage != null) {
exchange.setRequestPath(errorPage); exchange.setRequestPath(errorPage);
exchange.setRelativePath(errorPage); exchange.setRelativePath(errorPage);
@ -61,20 +86,20 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
IDToken idToken = account.getKeycloakSecurityContext().getToken(); IDToken idToken = account.getKeycloakSecurityContext().getToken();
if (idToken == null) return; if (idToken == null) return;
if (idToken.getSubject() != null) { if (idToken.getSubject() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_SUBJECT, idToken.getSubject()); exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_SUBJECT), idToken.getSubject());
} }
if (idToken.getPreferredUsername() != null) { if (idToken.getPreferredUsername() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_USERNAME, idToken.getPreferredUsername()); exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_USERNAME), idToken.getPreferredUsername());
} }
if (idToken.getEmail() != null) { if (idToken.getEmail() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_EMAIL, idToken.getEmail()); exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_EMAIL), idToken.getEmail());
} }
if (idToken.getName() != null) { if (idToken.getName() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_NAME, idToken.getName()); exchange.getRequestHeaders().put(httpHeaderNames.get(KEYCLOAK_NAME), idToken.getName());
} }
if (sendAccessToken) { 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); next.handleRequest(exchange);

View file

@ -1,11 +1,12 @@
package org.keycloak.proxy; 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.HttpHandler;
import io.undertow.server.HttpServerExchange; import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey; import io.undertow.util.AttachmentKey;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
import java.util.List;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -47,10 +48,42 @@ public class ConstraintMatcherHandler implements HttpHandler {
} }
return; 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"); log.debug("found constraint");
exchange.getSecurityContext().setAuthenticationRequired(); exchange.getSecurityContext().setAuthenticationRequired();
exchange.putAttachment(CONSTRAINT_KEY, match); exchange.putAttachment(CONSTRAINT_KEY, match);
securedHandler.handleRequest(exchange); securedHandler.handleRequest(exchange);
} }
private boolean isSuccessfulAuthenticatedMethodFound(HttpServerExchange exchange) {
boolean successfulAuthenticatedMethodFound = false;
List<AuthenticationMechanism> 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;
}
} }

View file

@ -3,10 +3,7 @@ package org.keycloak.proxy;
import org.codehaus.jackson.annotate.JsonProperty; import org.codehaus.jackson.annotate.JsonProperty;
import org.keycloak.representations.adapters.config.AdapterConfig; import org.keycloak.representations.adapters.config.AdapterConfig;
import java.util.HashSet; import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -41,6 +38,8 @@ public class ProxyConfig {
protected boolean sendAccessToken; protected boolean sendAccessToken;
@JsonProperty("applications") @JsonProperty("applications")
protected List<Application> applications = new LinkedList<Application>(); protected List<Application> applications = new LinkedList<Application>();
@JsonProperty("header-names")
private Map<String,String> headerNames = new HashMap<>();
public String getBindAddress() { public String getBindAddress() {
return bindAddress; return bindAddress;
@ -154,6 +153,14 @@ public class ProxyConfig {
this.sendAccessToken = sendAccessToken; this.sendAccessToken = sendAccessToken;
} }
public void setHeaderNames(Map<String, String> headerNames) {
this.headerNames = headerNames;
}
public Map<String, String> getHeaderNames() {
return headerNames;
}
public static class Application { public static class Application {
@JsonProperty("base-path") @JsonProperty("base-path")
protected String basePath; protected String basePath;
@ -212,6 +219,8 @@ public class ProxyConfig {
protected boolean permit; protected boolean permit;
@JsonProperty("authenticate") @JsonProperty("authenticate")
protected boolean authenticate; protected boolean authenticate;
@JsonProperty("permit-and-inject")
protected boolean permitAndInject;
public String getPattern() { public String getPattern() {
return pattern; return pattern;
@ -253,6 +262,14 @@ public class ProxyConfig {
this.authenticate = authenticate; this.authenticate = authenticate;
} }
public boolean isPermitAndInject() {
return permitAndInject;
}
public void setPermitAndInject(boolean permitAndInject) {
this.permitAndInject = permitAndInject;
}
public Set<String> getMethods() { public Set<String> getMethods() {
return methods; return methods;
} }

View file

@ -51,10 +51,7 @@ import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey; import java.security.PrivateKey;
import java.security.cert.Certificate; import java.security.cert.Certificate;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.util.HashSet; import java.util.*;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
/** /**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a> * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
@ -76,6 +73,8 @@ public class ProxyServerBuilder {
protected HttpHandler proxyHandler; protected HttpHandler proxyHandler;
protected boolean sendAccessToken; protected boolean sendAccessToken;
protected Map<String, String> headerNameConfig;
public ProxyServerBuilder target(String uri) { public ProxyServerBuilder target(String uri) {
SimpleProxyClientProvider provider = null; SimpleProxyClientProvider provider = null;
try { try {
@ -98,6 +97,12 @@ public class ProxyServerBuilder {
this.sendAccessToken = flag; this.sendAccessToken = flag;
return this; return this;
} }
public ProxyServerBuilder headerNameConfig(Map<String, String> headerNameConfig) {
this.headerNameConfig = headerNameConfig;
return this;
}
public ApplicationBuilder application(AdapterConfig config) { public ApplicationBuilder application(AdapterConfig config) {
return new ApplicationBuilder(config); return new ApplicationBuilder(config);
} }
@ -169,6 +174,11 @@ public class ProxyServerBuilder {
return this; return this;
} }
public ConstraintBuilder injectIfAuthenticated() {
semantic = SecurityInfo.EmptyRoleSemantic.PERMIT_AND_INJECT_IF_AUTHENTICATED;
return this;
}
public ConstraintBuilder excludedMethods(Set<String> excludedMethods) { public ConstraintBuilder excludedMethods(Set<String> excludedMethods) {
this.excludedMethods = excludedMethods; this.excludedMethods = excludedMethods;
return this; return this;
@ -222,7 +232,7 @@ public class ProxyServerBuilder {
errorPage = base + "/" + errorPage; errorPage = base + "/" + errorPage;
} }
} }
handler = new ConstraintAuthorizationHandler(handler, errorPage, sendAccessToken); handler = new ConstraintAuthorizationHandler(handler, errorPage, sendAccessToken, headerNameConfig);
handler = new ProxyAuthenticationCallHandler(handler); handler = new ProxyAuthenticationCallHandler(handler);
handler = new ConstraintMatcherHandler(matches, handler, toWrap, errorPage); handler = new ConstraintMatcherHandler(matches, handler, toWrap, errorPage);
final List<AuthenticationMechanism> mechanisms = new LinkedList<AuthenticationMechanism>(); final List<AuthenticationMechanism> mechanisms = new LinkedList<AuthenticationMechanism>();
@ -373,6 +383,7 @@ public class ProxyServerBuilder {
if (constraint.isDeny()) constraintBuilder.deny(); if (constraint.isDeny()) constraintBuilder.deny();
if (constraint.isPermit()) constraintBuilder.permit(); if (constraint.isPermit()) constraintBuilder.permit();
if (constraint.isAuthenticate()) constraintBuilder.authenticate(); if (constraint.isAuthenticate()) constraintBuilder.authenticate();
if (constraint.isPermitAndInject()) constraintBuilder.injectIfAuthenticated();
constraintBuilder.add(); constraintBuilder.add();
} }
} }
@ -383,6 +394,7 @@ public class ProxyServerBuilder {
public static void initOptions(ProxyConfig config, ProxyServerBuilder builder) { public static void initOptions(ProxyConfig config, ProxyServerBuilder builder) {
builder.sendAccessToken(config.isSendAccessToken()); builder.sendAccessToken(config.isSendAccessToken());
builder.headerNameConfig(config.getHeaderNames());
if (config.getBufferSize() != null) builder.setBufferSize(config.getBufferSize()); if (config.getBufferSize() != null) builder.setBufferSize(config.getBufferSize());
if (config.getBuffersPerRegion() != null) builder.setBuffersPerRegion(config.getBuffersPerRegion()); if (config.getBuffersPerRegion() != null) builder.setBuffersPerRegion(config.getBuffersPerRegion());
if (config.getIoThreads() != null) builder.setIoThreads(config.getIoThreads()); if (config.getIoThreads() != null) builder.setIoThreads(config.getIoThreads());

View file

@ -46,8 +46,12 @@ public class SecurityInfo<T extends SecurityInfo> implements Cloneable {
/** /**
* Mandate authentication but authorize access as no roles to check against. * 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; private volatile EmptyRoleSemantic emptyRoleSemantic = EmptyRoleSemantic.DENY;