Merge pull request #871 from patriot1burke/master

proxy distro and docs
This commit is contained in:
Bill Burke 2014-11-24 19:22:44 -05:00
commit 8c451af3f5
17 changed files with 549 additions and 21 deletions

View file

@ -19,6 +19,9 @@ import java.util.Date;
* @version $Revision: 1 $
*/
public class CertificateUtils {
static {
BouncyIntegration.init();
}
public static X509Certificate generateV3Certificate(KeyPair keyPair, PrivateKey caPrivateKey, X509Certificate caCert, String subject) throws Exception {
X509V3CertificateGenerator certGen = new X509V3CertificateGenerator();

View file

@ -39,6 +39,7 @@
<module>theme-template-zip</module>
<module>war-zip</module>
<module>war-dist</module>
<module>proxy</module>
<module>appliance-dist</module>
<module>src-dist</module>
</modules>

30
distribution/proxy/assembly.xml Executable file
View file

@ -0,0 +1,30 @@
<assembly>
<id>war-dist</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<dependencySets>
<dependencySet>
<unpack>false</unpack>
<useTransitiveDependencies>true</useTransitiveDependencies>
<useTransitiveFiltering>true</useTransitiveFiltering>
<includes>
<include>org.keycloak:launcher</include>
</includes>
<outputFileNameMapping>${artifact.artifactId}.${artifact.extension}</outputFileNameMapping>
<outputDirectory>bin</outputDirectory>
</dependencySet>
<dependencySet>
<unpack>false</unpack>
<useTransitiveDependencies>true</useTransitiveDependencies>
<useTransitiveFiltering>true</useTransitiveFiltering>
<includes>
<include>org.keycloak:keycloak-proxy-server</include>
</includes>
<outputDirectory>lib</outputDirectory>
</dependencySet>
</dependencySets>
</assembly>

58
distribution/proxy/pom.xml Executable file
View file

@ -0,0 +1,58 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0.Beta2-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>keycloak-proxy-dist</artifactId>
<packaging>pom</packaging>
<name>Proxy Distro</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>launcher</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-proxy-server</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-assembly-plugin</artifactId>
<version>2.4</version>
<executions>
<execution>
<id>assemble</id>
<phase>package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<descriptors>
<descriptor>assembly.xml</descriptor>
</descriptors>
<outputDirectory>
target
</outputDirectory>
<workDirectory>
target/assembly/work
</workDirectory>
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -39,6 +39,7 @@
<!ENTITY Clustering SYSTEM "modules/clustering.xml">
<!ENTITY ApplicationClustering SYSTEM "modules/application-clustering.xml">
<!ENTITY MultiTenancy SYSTEM "modules/multi-tenancy.xml">
<!ENTITY Proxy SYSTEM "modules/proxy.xml">
]>
<book>
@ -135,6 +136,7 @@ This one is short
&SecurityVulnerabilities;
&Clustering;
&ApplicationClustering;
&Proxy;
&Migration;
</book>

View file

@ -0,0 +1,277 @@
<chapter id="proxy">
<title>Keycloak Security Proxy</title>
<para>
Keycloak has an HTTP(S) proxy that you can put in front of web applications and services where it is not possible
to install the keycloak adapter. You can set up URL filters so that certain URLs are secured either by browser login
and/or bearer token authentication. You can also define role constraints for URL patterns within your applications.
</para>
<section>
<title>Proxy Install and Run</title>
<para>Download the keycloak proxy distribution from the Keycloak download pages and unzip it.
<programlisting>
$ unzip keycloak-proxy-dist.zip
</programlisting>
</para>
<para>
To run it you must have a proxy config file (which we'll discuss in a moment).
<programlisting>
$ java -jar bin/launcher.jar [your-config.json]
</programlisting>
</para>
<para>
If you do not specify a path to the proxy config file, the launcher will look in the current working directory
for the file named <literal>proxy.json</literal>
</para>
</section>
<section>
<title>Proxy Configuration</title>
<para>
Here's an example configuration file.
<programlisting><![CDATA[
{
"target-url": "http://localhost:8082",
"bind-address": "localhost",
"http-port": "8080",
"https-port": "8443",
"keystore": "classpath:ssl.jks",
"keystore-password": "password",
"key-password": "password",
"applications": [
{
"base-path": "/customer-portal",
"error-page": "/error.html",
"adapter-config": {
"realm": "demo",
"resource": "customer-portal",
"realm-public-key": "MIGfMA0GCSqGSIb",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "external",
"principal-attribute": "name",
"credentials": {
"secret": "password"
}
}
,
"constraints": [
{
"pattern": "/users/*",
"roles-allowed": [
"user"
]
},
{
"pattern": "/admins/*",
"roles-allowed": [
"admin"
]
},
{
"pattern": "/users/permit",
"permit": true
},
{
"pattern": "/users/deny",
"deny": true
}
]
}
]
}]]>
</programlisting>
</para>
<section>
<title>Basic Config</title>
<para>
The basic configuration options for the server are as follows:
<variablelist>
<varlistentry>
<term>target-url</term>
<listitem>
<para>
The URL this server is proxying <emphasis>REQUIRED.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>bind-address</term>
<listitem>
<para>
DNS name or IP address to bind the proxy server's sockets to.
<emphasis>OPTIONAL.</emphasis>. The default value is <emphasis>localhost</emphasis>
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>http-port</term>
<listitem>
<para>
Port to listen for HTTP requests. If you do not specify this value, then the proxy will
not listen for regular HTTP requests.
<emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>https-port</term>
<listitem>
<para>
Port to listen for HTTPS requests. If you do not specify this value, then the proxy will
not listen for HTTPS requests.
<emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>keystore</term>
<listitem>
<para>
Path to a Java keystore file that contains private key and certificate for the server to be
able to handle HTTPS requests. Can be a file path, or, if you prefix it with <literal>classpath:</literal>
it will look for this file in the classpath.
<emphasis>OPTIONAL.</emphasis>. If you have enabled HTTPS, but have not defined a keystore, the proxy
will auto-generate a self-signed certificate and use that.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>buffer-size</term>
<listitem>
<para>
HTTP server socket buffer size. Usually the default is good enough. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>buffers-per-region</term>
<listitem>
<para>
HTTP server socket buffers per region. Usually the default is good enough. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>io-threads</term>
<listitem>
<para>
Number of threads to handle IO. Usually default is good enough. <emphasis>OPTIONAL.</emphasis>.
The default is the number of available processors * 2.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>worker-threads</term>
<listitem>
<para>
Number of threads to handle requests. Usually the default is good enough. <emphasis>OPTIONAL.</emphasis>.
The default is the number of available processors * 16.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
<section>
<title>Application Config</title>
<para>
Next under the <literal>applications</literal> array attribute, you can define one or more applications per host you are proxying.
<variablelist>
<varlistentry>
<term>base-path</term>
<listitem>
<para>
The base context root for the application. Must start with '/' <emphasis>REQUIRED.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>error-page</term>
<listitem>
<para>
If the proxy has an error, it will display the target application's error page relative URL <emphasis>OPTIONAL.</emphasis>.
This is a relative path to the base-path. In the example above it would be <literal>/customer-portal/error.html</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>adapter-config</term>
<listitem>
<para>
<emphasis>REQUIRED.</emphasis>. Same configuration as any other keycloak adapter. See <link linkend='adapter-config'>Adapter Config</link>
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
<section>
<title>Constraint Config</title>
<para>
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 specific URL pattern. You can specify roles allowed for that path as well. More specific constraints will take
precedence over more general ones.
<variablelist>
<varlistentry>
<term>pattern</term>
<listitem>
<para>
URL pattern to match relative to the base-path of the application. Must start with '/' <emphasis>REQUIRED.</emphasis>.
You may only have one wildcard and it must come at the end of the pattern. Valid <literal>/foo/bar/*</literal> and <literal>/foo/*.txt</literal>
Not valid: <literal>/*/foo/*</literal>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>roles-allowed</term>
<listitem>
<para>
Array of strings of roles allowed to access this url pattern. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>methods</term>
<listitem>
<para>
Array of strings of HTTP methods that will exclusively match this pattern and HTTP request. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>excluded-methods</term>
<listitem>
<para>
Array of strings of HTTP methods that will be ignored when match this pattern. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>deny</term>
<listitem>
<para>
Deny all access to this URL pattern. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>permit</term>
<listitem>
<para>
Permit all access without requiring authentication or a role mapping. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
<varlistentry>
<term>authenticate</term>
<listitem>
<para>
Require authentication for this pattern, but no role mapping. <emphasis>OPTIONAL.</emphasis>.
</para>
</listitem>
</varlistentry>
</variablelist>
</para>
</section>
</section>
</section>
</chapter>

View file

@ -16,21 +16,15 @@
*/
package org.keycloak.adapters.undertow;
import io.undertow.security.api.AuthenticatedSessionManager;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.session.Session;
import io.undertow.server.session.SessionConfig;
import io.undertow.server.session.SessionListener;
import io.undertow.server.session.SessionManager;
import io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler;
import org.jboss.logging.Logger;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
/**
* Manages relationship to users and sessions so that forced admin logout can be implemented
@ -40,7 +34,6 @@ import java.util.concurrent.ConcurrentHashMap;
*/
public class UndertowUserSessionManagement implements SessionListener {
private static final Logger log = Logger.getLogger(UndertowUserSessionManagement.class);
private static final String AUTH_SESSION_NAME = CachedAuthenticatedSessionHandler.class.getName() + ".AuthenticatedSession";
protected volatile boolean registered;
public void login(SessionManager manager) {
@ -67,7 +60,7 @@ public class UndertowUserSessionManagement implements SessionListener {
log.debug("logoutHttpSession: " + httpSessionId);
Session session = getSessionById(manager, httpSessionId);
try {
session.invalidate(null);
if (session != null) session.invalidate(null);
} catch (Exception e) {
log.warnf("Session %s not present or already invalidated.", httpSessionId);
}
@ -115,16 +108,6 @@ public class UndertowUserSessionManagement implements SessionListener {
@Override
public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) {
// Look up the single session id associated with this session (if any)
String username = getUsernameFromSession(session);
log.debugf("Session destroyed for user: %s, sessionId: %s", username, session.getId());
}
protected String getUsernameFromSession(Session session) {
AuthenticatedSessionManager.AuthenticatedSession authSession = (AuthenticatedSessionManager.AuthenticatedSession) session.getAttribute(AUTH_SESSION_NAME);
if (authSession == null) return null;
return authSession.getAccount().getPrincipal().getName();
}

View file

@ -108,7 +108,7 @@
<module>events</module>
<module>model</module>
<module>integration</module>
<module>proxy/proxy-server</module>
<module>proxy</module>
<module>picketlink</module>
<module>federation</module>
<module>services</module>

43
proxy/launcher/pom.xml Executable file
View file

@ -0,0 +1,43 @@
<?xml version="1.0"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0.Beta2-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>launcher</artifactId>
<name>Keycloak Launcher</name>
<description/>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>org.keycloak.Launcher</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>${maven.compiler.source}</source>
<target>${maven.compiler.target}</target>
</configuration>
</plugin>
</plugins>
</build>
</project>

View file

@ -0,0 +1,66 @@
package org.keycloak;
import java.io.File;
import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Launcher {
public static File getHome() {
String launcherPath = Launcher.class.getName().replace('.', '/') + ".class";
URL jarfile = Launcher.class.getClassLoader().getResource(launcherPath);
if (jarfile != null) {
Matcher m = Pattern.compile("jar:(file:.*)!/" + launcherPath).matcher(jarfile.toString());
if (m.matches()) {
try {
File jarPath = new File(new URI(m.group(1)));
File libPath = jarPath.getParentFile().getParentFile();
System.out.println("Home directory: " + libPath.toString());
if (!libPath.exists()) {
System.exit(1);
}
return libPath;
} catch (URISyntaxException e) {
throw new RuntimeException(e);
}
}
} else {
System.err.println("jar file null: " + launcherPath);
}
return null;
}
public static void main(String[] args) throws Exception {
File home = getHome();
File lib = new File(home, "lib");
if (!lib.exists()) {
System.err.println("Could not find lib directory: " + lib.toString());
System.exit(1);
}
List<URL> jars = new ArrayList<URL>();
for (File file : lib.listFiles()) {
jars.add(file.toURI().toURL());
}
URL[] urls = jars.toArray(new URL[jars.size()]);
URLClassLoader loader = new URLClassLoader(urls, Launcher.class.getClassLoader());
Class mainClass = loader.loadClass("org.keycloak.proxy.Main");
Method mainMethod = null;
for (Method m : mainClass.getMethods()) if (m.getName().equals("main")) { mainMethod = m; break; }
Object obj = args;
mainMethod.invoke(null, obj);
}
}

31
proxy/pom.xml Executable file
View file

@ -0,0 +1,31 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<artifactId>keycloak-parent</artifactId>
<groupId>org.keycloak</groupId>
<version>1.1.0.Beta2-SNAPSHOT</version>
<relativePath>../pom.xml</relativePath>
</parent>
<name>Model Parent</name>
<description/>
<modelVersion>4.0.0</modelVersion>
<artifactId>keycloak-proxy-pom</artifactId>
<packaging>pom</packaging>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-deploy-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
<modules>
<module>launcher</module>
<module>proxy-server</module>
</modules>
</project>

View file

@ -18,7 +18,6 @@
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<version>${jboss.logging.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
@ -63,7 +62,6 @@
<dependency>
<groupId>io.undertow</groupId>
<artifactId>undertow-core</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>

View file

@ -4,6 +4,7 @@ import io.undertow.security.handlers.AuthenticationConstraintHandler;
import io.undertow.server.HttpHandler;
import io.undertow.server.HttpServerExchange;
import io.undertow.util.AttachmentKey;
import org.jboss.logging.Logger;
import org.keycloak.KeycloakSecurityContext;
/**
@ -11,6 +12,7 @@ import org.keycloak.KeycloakSecurityContext;
* @version $Revision: 1 $
*/
public class ConstraintMatcherHandler implements HttpHandler {
protected static Logger log = Logger.getLogger(ConstraintMatcherHandler.class);
public static final AttachmentKey<SingleConstraintMatch> CONSTRAINT_KEY = AttachmentKey.create(SingleConstraintMatch.class);
protected SecurityPathMatches matcher;
protected HttpHandler securedHandler;
@ -26,6 +28,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();
if (match == null || (match.getRequiredRoles().isEmpty() && match.getEmptyRoleSemantic() == SecurityInfo.EmptyRoleSemantic.PERMIT)) {
unsecuredHandler.handleRequest(exchange);
@ -44,6 +47,7 @@ public class ConstraintMatcherHandler implements HttpHandler {
}
return;
}
log.debug("found constraint");
exchange.getSecurityContext().setAuthenticationRequired();
exchange.putAttachment(CONSTRAINT_KEY, match);
securedHandler.handleRequest(exchange);

View file

@ -0,0 +1,28 @@
package org.keycloak.proxy;
import io.undertow.Undertow;
import java.io.File;
import java.io.FileInputStream;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class Main {
public static void main(String[] args) throws Exception {
String jsonConfig = "proxy.json";
if (args.length > 0) jsonConfig = args[0];
File file = new File(jsonConfig);
if (!file.exists()) {
System.err.println("No proxy config argument and could not find default file proxy.json");
System.exit(1);
return;
}
FileInputStream fis = new FileInputStream(file);
Undertow proxyServer = ProxyServerBuilder.build(fis);
proxyServer.start();
}
}

View file

@ -137,6 +137,7 @@ public class ProxyServerBuilder {
}
public ConstraintBuilder constraint(String pattern) {
log.debugv("add constraint: {0}", pattern);
return new ConstraintBuilder(pattern);
}

View file

@ -2,6 +2,9 @@
"bind-address": "localhost",
"http-port": "8080",
"https-port": "8443",
"keystore": "classpath:ssl.jks",
"keystore-password": "password",
"key-password": "password",
"target-url": "http://localhost:8082",
"applications": [
{

Binary file not shown.