fix proxy
This commit is contained in:
parent
e94f7ca741
commit
38073d2aaa
10 changed files with 142 additions and 63 deletions
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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": [
|
||||
|
|
Loading…
Reference in a new issue