fix proxy

This commit is contained in:
Bill Burke 2014-12-05 17:38:56 -05:00
parent e94f7ca741
commit 38073d2aaa
10 changed files with 142 additions and 63 deletions

View file

@ -30,6 +30,7 @@ $ java -jar bin/launcher.jar [your-config.json]
<programlisting><![CDATA[
{
"target-url": "http://localhost:8082",
"send-access-token": true,
"bind-address": "localhost",
"http-port": "8080",
"https-port": "8443",
@ -92,6 +93,15 @@ $ java -jar bin/launcher.jar [your-config.json]
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>send-access-token</term>
<listitem>
<para>
Boolean flag. If true, this will send the access token via the KEYCLOAK_ACCESS_TOKEN header to the
proxied server. <emphasis>OPTIONAL.</emphasis>. Default is false.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>bind-address</term>
<listitem>
@ -313,6 +323,15 @@ $ java -jar bin/launcher.jar [your-config.json]
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>KEYCLOAK_ACCESS_TOKEN</term>
<listitem>
<para>
Send the access token in this header if the proxy was configured to send it. This token can
be used to make bearer token requests.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>

View file

@ -17,13 +17,16 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
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");
protected HttpHandler next;
protected String errorPage;
protected boolean sendAccessToken;
public ConstraintAuthorizationHandler(String errorPage, HttpHandler next) {
this.errorPage = errorPage;
public ConstraintAuthorizationHandler(HttpHandler next, String errorPage, boolean sendAccessToken) {
this.next = next;
this.errorPage = errorPage;
this.sendAccessToken = sendAccessToken;
}
@Override
@ -57,7 +60,8 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
public void authenticatedRequest(KeycloakUndertowAccount account, HttpServerExchange exchange) throws Exception {
if (account != null) {
IDToken idToken = account.getKeycloakSecurityContext().getIdToken();
IDToken idToken = account.getKeycloakSecurityContext().getToken();
if (idToken == null) return;
if (idToken.getSubject() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_SUBJECT, idToken.getSubject());
}
@ -70,6 +74,9 @@ public class ConstraintAuthorizationHandler implements HttpHandler {
if (idToken.getName() != null) {
exchange.getRequestHeaders().put(KEYCLOAK_NAME, idToken.getName());
}
if (sendAccessToken) {
exchange.getRequestHeaders().put(KEYCLOAK_ACCESS_TOKEN, account.getKeycloakSecurityContext().getTokenString());
}
}
next.handleRequest(exchange);
}

View file

@ -29,7 +29,7 @@ public class ConstraintMatcherHandler implements HttpHandler {
@Override
public void handleRequest(HttpServerExchange exchange) throws Exception {
log.debugv("ConstraintMatcherHandler: {0}", exchange.getRelativePath());
SingleConstraintMatch match = matcher.getSecurityInfo(exchange.getRelativePath(), exchange.getRequestMethod().toString()).getMergedConstraint();
SingleConstraintMatch match = matcher.getSecurityInfo(exchange.getRelativePath(), exchange.getRequestMethod().toString());
if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.PERMIT)) {
unsecuredHandler.handleRequest(exchange);
return;

View file

@ -0,0 +1,35 @@
package org.keycloak.proxy;
import io.undertow.security.api.SecurityContext;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class ProxyAuthenticationCallHandler implements HttpHandler {
private final HttpHandler next;
public ProxyAuthenticationCallHandler(final HttpHandler next) {
this.next = next;
}
/**
* Only allow the request through if successfully authenticated or if authentication is not required.
*
* @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange)
*/
@Override
public void handleRequest(final HttpServerExchange exchange) throws Exception {
SecurityContext context = exchange.getSecurityContext();
if (context.authenticate()) {
if(!exchange.isComplete()) {
next.handleRequest(exchange);
}
} else {
exchange.endExchange();
}
}
}

View file

@ -37,6 +37,8 @@ public class ProxyConfig {
protected Boolean directBuffers;
@JsonProperty("target-url")
protected String targetUrl;
@JsonProperty("send-access-token")
protected boolean sendAccessToken;
@JsonProperty("applications")
protected List<Application> applications = new LinkedList<Application>();
@ -144,6 +146,14 @@ public class ProxyConfig {
this.applications = applications;
}
public boolean isSendAccessToken() {
return sendAccessToken;
}
public void setSendAccessToken(boolean sendAccessToken) {
this.sendAccessToken = sendAccessToken;
}
public static class Application {
@JsonProperty("base-path")
protected String basePath;

View file

@ -76,6 +76,7 @@ public class ProxyServerBuilder {
protected PathHandler root = new PathHandler(NOT_FOUND);
protected HttpHandler proxyHandler;
protected boolean sendAccessToken;
public ProxyServerBuilder target(String uri) {
SimpleProxyClientProvider provider = null;
@ -95,6 +96,10 @@ public class ProxyServerBuilder {
return this;
}
public ProxyServerBuilder sendAccessToken(boolean flag) {
this.sendAccessToken = flag;
return this;
}
public ApplicationBuilder application(AdapterConfig config) {
return new ApplicationBuilder(config);
}
@ -219,8 +224,8 @@ public class ProxyServerBuilder {
errorPage = base + "/" + errorPage;
}
}
handler = new ConstraintAuthorizationHandler(errorPage, handler);
handler = new AuthenticationCallHandler(handler);
handler = new ConstraintAuthorizationHandler(handler, errorPage, sendAccessToken);
handler = new ProxyAuthenticationCallHandler(handler);
handler = new ConstraintMatcherHandler(matches, handler, toWrap, errorPage);
final List<AuthenticationMechanism> mechanisms = new LinkedList<AuthenticationMechanism>();
mechanisms.add(new CachedAuthenticatedSessionMechanism());
@ -379,6 +384,7 @@ public class ProxyServerBuilder {
}
public static void initOptions(ProxyConfig config, ProxyServerBuilder builder) {
builder.sendAccessToken(config.isSendAccessToken());
if (config.getBufferSize() != null) builder.setBufferSize(config.getBufferSize());
if (config.getBuffersPerRegion() != null) builder.setBuffersPerRegion(config.getBuffersPerRegion());
if (config.getIoThreads() != null) builder.setIoThreads(config.getIoThreads());

View file

@ -1,36 +0,0 @@
/*
* JBoss, Home of Professional Open Source.
* Copyright 2014 Red Hat, Inc., and individual contributors
* as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.proxy;
/**
* @author Stuart Douglas
*/
public class SecurityPathMatch {
private final SingleConstraintMatch mergedConstraint;
SecurityPathMatch(final SingleConstraintMatch mergedConstraint) {
this.mergedConstraint = mergedConstraint;
}
SingleConstraintMatch getMergedConstraint() {
return mergedConstraint;
}
}

View file

@ -58,19 +58,19 @@ public class SecurityPathMatches {
extensionRoleInformation.isEmpty();
}
public SecurityPathMatch getSecurityInfo(final String path, final String method) {
public SingleConstraintMatch getSecurityInfo(final String path, final String method) {
RuntimeMatch currentMatch = new RuntimeMatch();
handleMatch(method, defaultPathSecurityInformation, currentMatch);
PathSecurityInformation match = exactPathRoleInformation.get(path);
if (match != null) {
handleMatch(method, match, currentMatch);
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
match = prefixPathRoleInformation.get(path);
if (match != null) {
handleMatch(method, match, currentMatch);
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
int qsPos = -1;
@ -83,7 +83,7 @@ public class SecurityPathMatches {
match = exactPathRoleInformation.get(part);
if (match != null) {
handleMatch(method, match, currentMatch);
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
qsPos = i;
extension = false;
@ -93,7 +93,7 @@ public class SecurityPathMatches {
match = prefixPathRoleInformation.get(part);
if (match != null) {
handleMatch(method, match, currentMatch);
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
} else if (c == '.') {
if (!extension) {
@ -107,12 +107,12 @@ public class SecurityPathMatches {
match = extensionRoleInformation.get(ext);
if (match != null) {
handleMatch(method, match, currentMatch);
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
}
}
}
return new SecurityPathMatch(mergeConstraints(currentMatch));
return mergeConstraints(currentMatch);
}
/**

View file

@ -62,6 +62,11 @@ import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;
import java.io.File;
import java.io.IOException;
@ -90,6 +95,7 @@ public class ProxyTest {
public static class SendUsernameServlet extends HttpServlet {
@Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
String requestURI = req.getRequestURI();
resp.setContentType("text/plain");
OutputStream stream = resp.getOutputStream();
stream.write(req.getRequestURL().toString().getBytes());
@ -104,6 +110,34 @@ public class ProxyTest {
String name = headers.nextElement();
System.out.println(name +": " + req.getHeader(name));
}
if (requestURI.contains("/bearer")) {
Client client = ClientBuilder.newClient();
try {
String appBase = "http://localhost:8080/customer-portal";
WebTarget target = client.target(appBase + "/call-bearer");
Response response = null;
response = target.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer CRAP")
.get();
Assert.assertEquals(401, response.getStatus());
response.close();
response = target.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + req.getHeader("KEYCLOAK_ACCESS_TOKEN"))
.get();
Assert.assertEquals(200, response.getStatus());
String data = response.readEntity(String.class);
response.close();
stream.write(data.getBytes());
} finally {
client.close();
}
} else if (requestURI.contains("/call-bearer")) {
stream.write("bearer called".getBytes());
}
}
@Override
protected void doPost(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
@ -151,20 +185,6 @@ public class ProxyTest {
public static void initProxy() throws Exception {
initTomcat();
InputStream is = ProxyTest.class.getResourceAsStream("/proxy-config.json");
/*
ProxyServerBuilder builder = new ProxyServerBuilder().addHttpListener(8080, "localhost");
AdapterConfig config = KeycloakDeploymentBuilder.loadAdapterConfig(is);
builder.target("http://localhost:8082")
.application(config)
.base("/customer-portal")
.errorPage("/error.html")
.constraint("/users/*").role("user").add()
.constraint("/admins/*").role("admin").add()
.constraint("/users/permit").permit().add()
.constraint("/users/deny").deny().add()
.add();
*/
proxyServer = ProxyServerBuilder.build(is);
proxyServer.start();
@ -221,6 +241,11 @@ public class ProxyTest {
Assert.assertTrue(pageSource.contains("customer-portal/users"));
Assert.assertTrue(pageSource.contains("count:1")); // test http session
driver.navigate().to(baseUrl + "/customer-portal/bearer");
pageSource = driver.getPageSource();
Assert.assertTrue(pageSource.contains("bearer called"));
driver.navigate().to(baseUrl + "/customer-portal/users/deny");
Assert.assertEquals(driver.getCurrentUrl(), baseUrl + "/customer-portal/users/deny");
pageSource = driver.getPageSource();

View file

@ -6,6 +6,7 @@
"keystore-password": "password",
"key-password": "password",
"target-url": "http://localhost:8082",
"send-access-token": true,
"applications": [
{
"base-path": "/customer-portal",
@ -29,6 +30,18 @@
"user"
]
},
{
"pattern": "/call-bearer/*",
"roles-allowed": [
"user"
]
},
{
"pattern": "/bearer/*",
"roles-allowed": [
"user"
]
},
{
"pattern": "/admins/*",
"roles-allowed": [