KEYCLOAK-1103 - First draft
This commit is contained in:
parent
f61a9d7a70
commit
7659b1aa18
5 changed files with 107 additions and 27 deletions
|
@ -6,43 +6,58 @@ 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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
* @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");
|
||||
|
||||
private final Map<String, HttpString> 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<String, String> headerNames) {
|
||||
this.next = next;
|
||||
this.errorPage = errorPage;
|
||||
this.sendAccessToken = sendAccessToken;
|
||||
|
||||
this.httpHeaderNames = new HashMap<>();
|
||||
this.httpHeaderNames.put("KEYCLOAK_SUBJECT", new HttpString(headerNames.getOrDefault("keycloak-subject", "KEYCLOAK_SUBJECT")));
|
||||
this.httpHeaderNames.put("KEYCLOAK_USERNAME", new HttpString(headerNames.getOrDefault("keycloak-username", "KEYCLOAK_USERNAME")));
|
||||
this.httpHeaderNames.put("KEYCLOAK_EMAIL", new HttpString(headerNames.getOrDefault("keycloak-email", "KEYCLOAK_EMAIL")));
|
||||
this.httpHeaderNames.put("KEYCLOAK_NAME", new HttpString(headerNames.getOrDefault("keycloak-name", "KEYCLOAK_NAME")));
|
||||
this.httpHeaderNames.put("KEYCLOAK_ACCESS_TOKEN", new HttpString(headerNames.getOrDefault("keycloak-access-token", "KEYCLOAK_ACCESS_TOKEN")));
|
||||
}
|
||||
|
||||
@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.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 +76,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);
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -47,10 +48,41 @@ public class ConstraintMatcherHandler implements HttpHandler {
|
|||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.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<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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -41,6 +38,8 @@ public class ProxyConfig {
|
|||
protected boolean sendAccessToken;
|
||||
@JsonProperty("applications")
|
||||
protected List<Application> applications = new LinkedList<Application>();
|
||||
@JsonProperty("header-names")
|
||||
private Map<String,String> headerNames = new HashMap<>();
|
||||
|
||||
public String getBindAddress() {
|
||||
return bindAddress;
|
||||
|
@ -154,6 +153,14 @@ public class ProxyConfig {
|
|||
this.sendAccessToken = sendAccessToken;
|
||||
}
|
||||
|
||||
public void setHeaderNames(Map<String, String> headerNames) {
|
||||
this.headerNames = headerNames;
|
||||
}
|
||||
|
||||
public Map<String, String> 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("inject-if-authenticated")
|
||||
protected boolean injectIfAuthenticated;
|
||||
|
||||
public String getPattern() {
|
||||
return pattern;
|
||||
|
@ -253,6 +262,14 @@ public class ProxyConfig {
|
|||
this.authenticate = authenticate;
|
||||
}
|
||||
|
||||
public boolean isInjectIfAuthenticated() {
|
||||
return injectIfAuthenticated;
|
||||
}
|
||||
|
||||
public void setInjectIfAuthenticated(boolean injectIfAuthenticated) {
|
||||
this.injectIfAuthenticated = injectIfAuthenticated;
|
||||
}
|
||||
|
||||
public Set<String> getMethods() {
|
||||
return methods;
|
||||
}
|
||||
|
|
|
@ -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 <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||
|
@ -76,6 +73,8 @@ public class ProxyServerBuilder {
|
|||
protected HttpHandler proxyHandler;
|
||||
protected boolean sendAccessToken;
|
||||
|
||||
protected Map<String, String> 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<String, String> 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.INJECT_IF_AUTHENTICATED;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ConstraintBuilder excludedMethods(Set<String> 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<AuthenticationMechanism> mechanisms = new LinkedList<AuthenticationMechanism>();
|
||||
|
@ -373,6 +383,7 @@ public class ProxyServerBuilder {
|
|||
if (constraint.isDeny()) constraintBuilder.deny();
|
||||
if (constraint.isPermit()) constraintBuilder.permit();
|
||||
if (constraint.isAuthenticate()) constraintBuilder.authenticate();
|
||||
if (constraint.isInjectIfAuthenticated()) 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());
|
||||
|
|
|
@ -46,8 +46,12 @@ public class SecurityInfo<T extends 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.
|
||||
*/
|
||||
INJECT_IF_AUTHENTICATED;
|
||||
}
|
||||
|
||||
private volatile EmptyRoleSemantic emptyRoleSemantic = EmptyRoleSemantic.DENY;
|
||||
|
|
Loading…
Reference in a new issue