more jetty adapter

This commit is contained in:
Bill Burke 2014-11-07 18:34:53 -05:00
parent 31050e0580
commit 3805510e20
10 changed files with 232 additions and 72 deletions

View file

@ -0,0 +1,33 @@
<assembly>
<id>war-dist</id>
<formats>
<format>zip</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory></directory>
<includes>
<include>keycloak.mod</include>
</includes>
<outputDirectory>modules</outputDirectory>
</fileSet>
<fileSet>
<directory>${project.build.directory}/modules</directory>
<outputDirectory></outputDirectory>
</fileSet>
</fileSets>
<dependencySets>
<dependencySet>
<unpack>false</unpack>
<useTransitiveDependencies>true</useTransitiveDependencies>
<useTransitiveFiltering>true</useTransitiveFiltering>
<includes>
<include>org.keycloak:keycloak-jetty9-adapter</include>
</includes>
<outputDirectory>lib/keycloak</outputDirectory>
</dependencySet>
</dependencySets>
</assembly>

View file

@ -0,0 +1,11 @@
#
# Keycloak Jetty Adapter
#
[depend]
server
security
[lib]
lib/keycloak/*.jar

View file

@ -0,0 +1,53 @@
<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-jetty9-adapter-dist</artifactId>
<packaging>pom</packaging>
<name>Keycloak Jetty 9 Adapter Distro</name>
<description/>
<dependencies>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jetty9-adapter</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

@ -12,7 +12,7 @@
<artifactId>keycloak-jetty9-adapter</artifactId> <artifactId>keycloak-jetty9-adapter</artifactId>
<name>Keycloak Jetty 9 Integration</name> <name>Keycloak Jetty 9 Integration</name>
<properties> <properties>
<jetty9.version>9.1.0.v20131115</jetty9.version> <jetty9.version>9.2.4.v20141103</jetty9.version>
</properties> </properties>
<description /> <description />

View file

@ -5,7 +5,6 @@ import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AdapterTokenStore;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.CookieTokenStore; import org.keycloak.adapters.CookieTokenStore;
import org.keycloak.adapters.HttpFacade; import org.keycloak.adapters.HttpFacade;
import org.keycloak.adapters.KeycloakAccount; import org.keycloak.adapters.KeycloakAccount;
@ -13,8 +12,6 @@ import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator; import org.keycloak.adapters.RequestAuthenticator;
import java.util.Set;
/** /**
* Handle storage of token info in cookie. Per-request object. * Handle storage of token info in cookie. Per-request object.
* *

View file

@ -20,7 +20,6 @@ import org.keycloak.enums.TokenStore;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.security.Principal; import java.security.Principal;
import java.util.Set; import java.util.Set;
@ -70,8 +69,9 @@ public class JettyRequestAuthenticator extends RequestAuthenticator {
if (MimeTypes.Type.FORM_ENCODED.is(request.getContentType()) && HttpMethod.POST.is(request.getMethod())) { if (MimeTypes.Type.FORM_ENCODED.is(request.getContentType()) && HttpMethod.POST.is(request.getMethod())) {
Request base_request = (request instanceof Request) ? (Request) request : HttpChannel Request base_request = (request instanceof Request) ? (Request) request : HttpChannel
.getCurrentHttpChannel().getRequest(); .getCurrentHttpChannel().getRequest();
base_request.extractParameters(); MultiMap<String> formParameters = new MultiMap<String>();
session.setAttribute(FormAuthenticator.__J_POST, new MultiMap<String>(base_request.getParameters())); base_request.extractFormParameters(formParameters);
session.setAttribute(FormAuthenticator.__J_POST, formParameters);
} }
} }
} }
@ -134,7 +134,7 @@ public class JettyRequestAuthenticator extends RequestAuthenticator {
MultiMap<String> j_post = (MultiMap<String>) session.getAttribute(FormAuthenticator.__J_POST); MultiMap<String> j_post = (MultiMap<String>) session.getAttribute(FormAuthenticator.__J_POST);
if (j_post != null) { if (j_post != null) {
Request base_request = HttpChannel.getCurrentHttpChannel().getRequest(); Request base_request = HttpChannel.getCurrentHttpChannel().getRequest();
base_request.setParameters(j_post); base_request.setContentParameters(j_post);
} }
session.removeAttribute(FormAuthenticator.__J_URI); session.removeAttribute(FormAuthenticator.__J_URI);
session.removeAttribute(FormAuthenticator.__J_METHOD); session.removeAttribute(FormAuthenticator.__J_METHOD);

View file

@ -12,7 +12,6 @@ import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.RequestAuthenticator; import org.keycloak.adapters.RequestAuthenticator;
import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSession;
import java.util.Set;
/** /**
* Handle storage of token info in HTTP Session. Per-request object * Handle storage of token info in HTTP Session. Per-request object

View file

@ -1,20 +1,18 @@
package org.keycloak.adapters.jetty; package org.keycloak.adapters.jetty;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.http.MimeTypes;
import org.eclipse.jetty.security.Authenticator;
import org.eclipse.jetty.security.DefaultUserIdentity; import org.eclipse.jetty.security.DefaultUserIdentity;
import org.eclipse.jetty.security.ServerAuthException; import org.eclipse.jetty.security.ServerAuthException;
import org.eclipse.jetty.security.UserAuthentication; import org.eclipse.jetty.security.UserAuthentication;
import org.eclipse.jetty.security.authentication.FormAuthenticator; import org.eclipse.jetty.security.authentication.DeferredAuthentication;
import org.eclipse.jetty.security.authentication.LoginAuthenticator;
import org.eclipse.jetty.server.Authentication; import org.eclipse.jetty.server.Authentication;
import org.eclipse.jetty.server.HttpChannel; import org.eclipse.jetty.server.HttpChannel;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.UserIdentity; import org.eclipse.jetty.server.UserIdentity;
import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.server.handler.ContextHandler;
import org.eclipse.jetty.util.MultiMap;
import org.jboss.logging.Logger; import org.jboss.logging.Logger;
import org.keycloak.KeycloakPrincipal; import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterConstants; import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AdapterTokenStore;
@ -30,89 +28,97 @@ import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler; import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext; import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.enums.TokenStore; import org.keycloak.enums.TokenStore;
import org.keycloak.representations.adapters.config.AdapterConfig;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.servlet.ServletContext; import javax.servlet.ServletContext;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.InputStream; import java.io.InputStream;
import java.security.Principal;
import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Set; import java.util.Set;
/** /**
* @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 KeycloakJettyAuthenticator extends FormAuthenticator { public class KeycloakJettyAuthenticator extends LoginAuthenticator {
private static final org.jboss.logging.Logger log = Logger.getLogger(KeycloakJettyAuthenticator.class); private static final org.jboss.logging.Logger log = Logger.getLogger(KeycloakJettyAuthenticator.class);
protected AdapterDeploymentContext deploymentContext; protected AdapterDeploymentContext deploymentContext;
protected NodesRegistrationManagement nodesRegistrationManagement; protected NodesRegistrationManagement nodesRegistrationManagement;
protected AdapterConfig adapterConfig;
protected KeycloakConfigResolver configResolver;
public KeycloakJettyAuthenticator() { public KeycloakJettyAuthenticator() {
super(); super();
} }
public KeycloakJettyAuthenticator(String login, String error, boolean dispatch) { @Override
super(login, error, dispatch); public void setConfiguration(AuthConfiguration configuration) {
//super.setConfiguration(configuration);
initializeKeycloak();
} }
@Override @Override
public void setConfiguration(AuthConfiguration configuration) { public boolean secureResponse(ServletRequest req, ServletResponse res, boolean mandatory, Authentication.User validatedUser) throws ServerAuthException
super.setConfiguration(configuration); {
initializeKeycloak(); return true;
}
public AdapterConfig getAdapterConfig() {
return adapterConfig;
}
public void setAdapterConfig(AdapterConfig adapterConfig) {
this.adapterConfig = adapterConfig;
}
public KeycloakConfigResolver getConfigResolver() {
return configResolver;
}
public void setConfigResolver(KeycloakConfigResolver configResolver) {
this.configResolver = configResolver;
} }
@SuppressWarnings("UseSpecificCatch") @SuppressWarnings("UseSpecificCatch")
public void initializeKeycloak() { public void initializeKeycloak() {
nodesRegistrationManagement = new NodesRegistrationManagement();
String contextPath = ContextHandler.getCurrentContext().getContextPath(); String contextPath = ContextHandler.getCurrentContext().getContextPath();
ServletContext theServletContext = ContextHandler.getCurrentContext().getContext(contextPath); ServletContext theServletContext = ContextHandler.getCurrentContext().getContext(contextPath);
// Possible scenarios:
// 1) The deployment has a keycloak.config.resolver specified and it exists:
// Outcome: adapter uses the resolver
// 2) The deployment has a keycloak.config.resolver and isn't valid (doesn't exists, isn't a resolver, ...) :
// Outcome: adapter is left unconfigured
// 3) The deployment doesn't have a keycloak.config.resolver , but has a keycloak.json (or equivalent)
// Outcome: adapter uses it
// 4) The deployment doesn't have a keycloak.config.resolver nor keycloak.json (or equivalent)
// Outcome: adapter is left unconfigured
if (configResolver == null) {
String configResolverClass = theServletContext.getInitParameter("keycloak.config.resolver"); String configResolverClass = theServletContext.getInitParameter("keycloak.config.resolver");
if (configResolverClass != null) { if (configResolverClass != null) {
try { try {
KeycloakConfigResolver configResolver = (KeycloakConfigResolver) ContextHandler.getCurrentContext().getClassLoader().loadClass(configResolverClass).newInstance(); configResolver = (KeycloakConfigResolver) ContextHandler.getCurrentContext().getClassLoader().loadClass(configResolverClass).newInstance();
deploymentContext = new AdapterDeploymentContext(configResolver);
log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass); log.infov("Using {0} to resolve Keycloak configuration on a per-request basis.", configResolverClass);
} catch (Exception ex) { } catch (Exception ex) {
log.infov("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()}); log.infov("The specified resolver {0} could NOT be loaded. Keycloak is unconfigured and will deny all requests. Reason: {1}", new Object[]{configResolverClass, ex.getMessage()});
deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
} }
}
}
if (configResolver != null) {
deploymentContext = new AdapterDeploymentContext(configResolver);
} else if (adapterConfig != null) {
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(adapterConfig);
deploymentContext = new AdapterDeploymentContext(kd);
} else { } else {
InputStream configInputStream = getConfigInputStream(theServletContext); InputStream configInputStream = getConfigInputStream(theServletContext);
KeycloakDeployment kd;
if (configInputStream == null) { if (configInputStream == null) {
log.debug("No adapter configuration. Keycloak is unconfigured and will deny all requests."); deploymentContext = new AdapterDeploymentContext(new KeycloakDeployment());
kd = new KeycloakDeployment();
} else { } else {
kd = KeycloakDeploymentBuilder.build(configInputStream); deploymentContext = new AdapterDeploymentContext(KeycloakDeploymentBuilder.build(configInputStream));
}
deploymentContext = new AdapterDeploymentContext(kd);
log.debug("Keycloak is using a per-deployment configuration.");
} }
}
theServletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext); theServletContext.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
//AuthenticatedActionsValve actions = new AuthenticatedActionsValve(deploymentContext, getNext(), getContainer());
//setNext(actions);
nodesRegistrationManagement = new NodesRegistrationManagement();
} }
private static InputStream getJSONFromServletContext(ServletContext servletContext) { private static InputStream getJSONFromServletContext(ServletContext servletContext) {
@ -146,6 +152,8 @@ public class KeycloakJettyAuthenticator extends FormAuthenticator {
if (log.isTraceEnabled()) { if (log.isTraceEnabled()) {
log.trace("*** authenticate"); log.trace("*** authenticate");
} }
if (!mandatory)
return new DeferredAuthentication(this);
Request request = HttpChannel.getCurrentHttpChannel().getRequest(); Request request = HttpChannel.getCurrentHttpChannel().getRequest();
JettyHttpFacade facade = new JettyHttpFacade(request, (HttpServletResponse)res); JettyHttpFacade facade = new JettyHttpFacade(request, (HttpServletResponse)res);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade); KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
@ -189,7 +197,7 @@ public class KeycloakJettyAuthenticator extends FormAuthenticator {
} }
public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE"; public static final String TOKEN_STORE_NOTE = "TOKEN_STORE_NOTE";
protected AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) { public static AdapterTokenStore getTokenStore(Request request, HttpFacade facade, KeycloakDeployment resolvedDeployment) {
AdapterTokenStore store = (AdapterTokenStore)request.getAttribute(TOKEN_STORE_NOTE); AdapterTokenStore store = (AdapterTokenStore)request.getAttribute(TOKEN_STORE_NOTE);
if (store != null) { if (store != null) {
return store; return store;
@ -205,24 +213,61 @@ public class KeycloakJettyAuthenticator extends FormAuthenticator {
return store; return store;
} }
protected Authentication register(HttpServletRequest httpServletRequest, KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) { public static class KeycloakAuthentication extends UserAuthentication
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(principal.getKeycloakSecurityContext()); {
if (roles == null) { public KeycloakAuthentication(String method, UserIdentity userIdentity) {
roles = new HashSet<String>(); super(method, userIdentity);
} }
Request request = (Request) httpServletRequest;
Authentication authentication = request.getAuthentication();
if (!(authentication instanceof UserAuthentication)) {
Subject theSubject = new Subject();
String[] theRoles = new String[roles.size()];
roles.toArray(theRoles);
UserIdentity userIdentity = new DefaultUserIdentity(theSubject, principal, theRoles); @Override
authentication = new UserAuthentication(getAuthMethod(), userIdentity); public void logout() {
Request request = HttpChannel.getCurrentHttpChannel().getRequest();
logoutCurrent(request);
}
}
public static void logoutCurrent(Request request) {
AdapterDeploymentContext deploymentContext = (AdapterDeploymentContext)request.getAttribute(AdapterDeploymentContext.class.getName());
KeycloakSecurityContext ksc = (KeycloakSecurityContext)request.getAttribute(KeycloakSecurityContext.class.getName());
if (ksc != null) {
JettyHttpFacade facade = new JettyHttpFacade(request, null);
KeycloakDeployment deployment = deploymentContext.resolveDeployment(facade);
if (ksc instanceof RefreshableKeycloakSecurityContext) {
((RefreshableKeycloakSecurityContext) ksc).logout(deployment);
}
AdapterTokenStore tokenStore = getTokenStore(request, facade, deployment);
tokenStore.logout();
request.removeAttribute(KeycloakSecurityContext.class.getName());
}
}
protected Authentication register(Request request, KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
request.setAttribute(AdapterDeploymentContext.class.getName(), deploymentContext);
Authentication authentication = request.getAuthentication();
if (!(authentication instanceof KeycloakAuthentication)) {
UserIdentity userIdentity = createIdentity(principal);
authentication = new KeycloakAuthentication(getAuthMethod(), userIdentity);
request.setAuthentication(authentication); request.setAuthentication(authentication);
} }
return authentication; return authentication;
} }
public static UserIdentity createIdentity(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
Set<String> roles = AdapterUtils.getRolesFromSecurityContext(principal.getKeycloakSecurityContext());
if (roles == null) {
roles = new HashSet<String>();
}
Subject theSubject = new Subject();
String[] theRoles = new String[roles.size()];
roles.toArray(theRoles);
return new DefaultUserIdentity(theSubject, principal, theRoles);
}
} }

View file

@ -12,7 +12,7 @@
<artifactId>keycloak-testsuite-jetty9</artifactId> <artifactId>keycloak-testsuite-jetty9</artifactId>
<name>Keycloak Jetty 9 Integration TestSuite</name> <name>Keycloak Jetty 9 Integration TestSuite</name>
<properties> <properties>
<jetty9.version>9.1.0.v20131115</jetty9.version> <jetty9.version>9.2.4.v20141103</jetty9.version>
</properties> </properties>
<description /> <description />

View file

@ -84,6 +84,14 @@ public class Jetty9Test {
public static class SendUsernameServlet extends HttpServlet { public static class SendUsernameServlet extends HttpServlet {
@Override @Override
protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException { protected void doGet(final HttpServletRequest req, final HttpServletResponse resp) throws ServletException, IOException {
if (req.getPathInfo().endsWith("logout")) {
req.logout();
resp.setContentType("text/plain");
OutputStream stream = resp.getOutputStream();
stream.write("logout".getBytes());
return;
}
resp.setContentType("text/plain"); resp.setContentType("text/plain");
OutputStream stream = resp.getOutputStream(); OutputStream stream = resp.getOutputStream();
Principal principal = req.getUserPrincipal(); Principal principal = req.getUserPrincipal();
@ -199,6 +207,20 @@ public class Jetty9Test {
String currentUrl = driver.getCurrentUrl(); String currentUrl = driver.getCurrentUrl();
Assert.assertTrue(currentUrl.startsWith(LOGIN_URL)); Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
// test servletRequest.logout()
loginPage.login("bburke@redhat.com", "password");
System.out.println("Current url: " + driver.getCurrentUrl());
Assert.assertEquals(driver.getCurrentUrl(), "http://localhost:8080/customer-portal/");
pageSource = driver.getPageSource();
System.out.println(pageSource);
Assert.assertTrue(pageSource.contains("Bill Burke"));
driver.navigate().to("http://localhost:8080/customer-portal/logout");
pageSource = driver.getPageSource();
Assert.assertTrue(pageSource.contains("logout"));
driver.navigate().to("http://localhost:8080/customer-portal");
currentUrl = driver.getCurrentUrl();
Assert.assertTrue(currentUrl.startsWith(LOGIN_URL));
} }