Merge pull request #722 from mposolda/master

Http session replication working for example apps on Wildfly
This commit is contained in:
Marek Posolda 2014-10-01 19:14:14 +02:00
commit a75c9ee04e
21 changed files with 154 additions and 46 deletions

View file

@ -7,16 +7,16 @@ import java.security.Principal;
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
public class KeycloakPrincipal implements Principal, Serializable {
public class KeycloakPrincipal<T extends KeycloakSecurityContext> implements Principal, Serializable {
protected final String name;
protected final KeycloakSecurityContext context;
protected final T context;
public KeycloakPrincipal(String name, KeycloakSecurityContext context) {
public KeycloakPrincipal(String name, T context) {
this.name = name;
this.context = context;
}
public KeycloakSecurityContext getKeycloakSecurityContext() {
public T getKeycloakSecurityContext() {
return context;
}

View file

@ -2,7 +2,12 @@ package org.keycloak;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.util.Base64Url;
import org.keycloak.util.JsonSerialization;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
/**
@ -11,10 +16,12 @@ import java.io.Serializable;
*/
public class KeycloakSecurityContext implements Serializable {
protected String tokenString;
protected AccessToken token;
protected IDToken idToken;
protected String idTokenString;
// Don't store parsed tokens into HTTP session
protected transient AccessToken token;
protected transient IDToken idToken;
public KeycloakSecurityContext() {
}
@ -40,4 +47,32 @@ public class KeycloakSecurityContext implements Serializable {
public String getIdTokenString() {
return idTokenString;
}
// SERIALIZATION
private void writeObject(ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
}
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
token = parseToken(tokenString, AccessToken.class);
idToken = parseToken(idTokenString, IDToken.class);
}
// Just decode token without any verifications
private <T> T parseToken(String encoded, Class<T> clazz) throws IOException {
if (encoded == null)
return null;
String[] parts = encoded.split("\\.");
if (parts.length < 2 || parts.length > 3) throw new IllegalArgumentException("Parsing error");
byte[] bytes = Base64Url.decode(parts[1]);
return JsonSerialization.readValue(bytes, clazz);
}
}

View file

@ -3,6 +3,7 @@ package org.keycloak.representations;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonProperty;
import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
@ -13,7 +14,7 @@ import java.util.Set;
* @version $Revision: 1 $
*/
public class AccessToken extends IDToken {
public static class Access {
public static class Access implements Serializable {
@JsonProperty("roles")
protected Set<String> roles;
@JsonProperty("verify_caller")

View file

@ -1,13 +1,18 @@
package org.keycloak;
import junit.framework.Assert;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.jose.jws.JWSInput;
import org.keycloak.jose.jws.crypto.RSAProvider;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.util.JsonSerialization;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
@ -18,10 +23,7 @@ import java.security.KeyPairGenerator;
public class SkeletonKeyTokenTest {
@Test
public void testToken() throws Exception {
AccessToken token = new AccessToken();
token.id("111");
token.addAccess("foo").addRole("admin");
token.addAccess("bar").addRole("user");
AccessToken token = createSimpleToken();
String json = JsonSerialization.writeValueAsString(token);
token = JsonSerialization.readValue(json, AccessToken.class);
@ -34,7 +36,7 @@ public class SkeletonKeyTokenTest {
@Test
public void testRSA() throws Exception {
AccessToken token = new AccessToken();
AccessToken token = createSimpleToken();
token.id("111");
token.addAccess("foo").addRole("admin");
token.addAccess("bar").addRole("user");
@ -51,4 +53,57 @@ public class SkeletonKeyTokenTest {
Assert.assertEquals("111", token.getId());
Assert.assertTrue(RSAProvider.verify(input, keyPair.getPublic()));
}
@Test
public void testSerialization() throws Exception {
AccessToken token = createSimpleToken();
IDToken idToken = new IDToken();
idToken.setEmail("joe@email.cz");
KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair();
String encoded = new JWSBuilder()
.jsonContent(token)
.rsa256(keyPair.getPrivate());
String encodedIdToken = new JWSBuilder()
.jsonContent(idToken)
.rsa256(keyPair.getPrivate());
KeycloakSecurityContext ctx = new KeycloakSecurityContext(encoded, token, encodedIdToken, idToken);
KeycloakPrincipal principal = new KeycloakPrincipal("joe", ctx);
// Serialize
ByteArrayOutputStream bso = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bso);
oos.writeObject(principal);
oos.close();
// Deserialize
byte[] bytes = bso.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis);
principal = (KeycloakPrincipal)ois.readObject();
ctx = principal.getKeycloakSecurityContext();
token = ctx.getToken();
idToken = ctx.getIdToken();
System.out.println("Size of serialized principal: " + bytes.length);
Assert.assertEquals(encoded, ctx.getTokenString());
Assert.assertEquals(encodedIdToken, ctx.getIdTokenString());
Assert.assertEquals("111", token.getId());
Assert.assertEquals("111", token.getId());
Assert.assertTrue(token.getResourceAccess("foo").isUserInRole("admin"));
Assert.assertTrue(token.getResourceAccess("bar").isUserInRole("user"));
Assert.assertEquals("joe@email.cz", idToken.getEmail());
ois.close();
}
private AccessToken createSimpleToken() {
AccessToken token = new AccessToken();
token.id("111");
token.addAccess("foo").addRole("admin");
token.addAccess("bar").addRole("user");
return token;
}
}

View file

@ -3,6 +3,7 @@
<%@ page import="org.keycloak.representations.idm.RoleRepresentation" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %>
<%@ page session="false" %>
<html>
<head>
<title>Admin Interface</title>

View file

@ -2,6 +2,7 @@
<%@ page import="org.keycloak.representations.idm.RoleRepresentation" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1" %>
<%@ page session="false" %>
<html>
<head>
<title>Customer Admin Interface</title>

View file

@ -9,8 +9,8 @@
<title>Customer Session Page</title>
</head>
<body bgcolor="#E3F6CE">
<p>Your hostname: <%= UriUtils.getHostName() %></p>
<p>Your session ID: <%= request.getSession().getId() %></p>
<p>Your hostname: <b><%= UriUtils.getHostName() %></b></p>
<p>Your session ID: <b><%= request.getSession().getId() %></b></p>
<p>You visited this page <b><%= CustomerDatabaseClient.increaseAndGetCounter(request) %></b> times.</p>
<br><br>
</body>

View file

@ -4,6 +4,7 @@
<%@ page import="org.keycloak.example.CustomerDatabaseClient" %>
<%@ page import="org.keycloak.representations.IDToken" %>
<%@ page import="org.keycloak.util.KeycloakUriBuilder" %>
<%@ page session="false" %>
<html>
<head>
<title>Customer View Page</title>

View file

@ -1,5 +1,6 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page session="false" %>
<html>
<head>
<title>Product Admin Interface</title>

View file

@ -1,5 +1,6 @@
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page session="false" %>
<html>
<head>
<title>Servlet Logout</title>

View file

@ -3,6 +3,7 @@
<%@ page import="org.keycloak.ServiceUrlConstants" %>
<%@ page import="org.keycloak.example.oauth.ProductDatabaseClient" %>
<%@ page import="org.keycloak.util.KeycloakUriBuilder" %>
<%@ page session="false" %>
<html>
<head>
<title>Product View Page</title>

View file

@ -4,6 +4,7 @@
<%@ page import="org.keycloak.servlet.ServletOAuthClient" %>
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page session="false" %>
<html>
<head>
<title>Pull Page</title>

View file

@ -1,3 +1,5 @@
<%@ page import="org.keycloak.example.oauth.ProductDatabaseClient" %><%
<%@ page import="org.keycloak.example.oauth.ProductDatabaseClient" %>
<%@ page session="false" %>
<%
ProductDatabaseClient.redirect(request, response);
%>

View file

@ -104,18 +104,18 @@ public abstract class RequestAuthenticator {
protected void completeAuthentication(OAuthRequestAuthenticator oauth) {
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken());
final KeycloakPrincipal principal = new KeycloakPrincipal(oauth.getToken().getSubject(), session);
completeOAuthAuthentication(principal, session);
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(oauth.getToken().getSubject(), session);
completeOAuthAuthentication(principal);
}
protected abstract void completeOAuthAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session);
protected abstract void completeBearerAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session);
protected abstract void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
protected abstract void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
protected abstract boolean isCached();
protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) {
RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, bearer.getTokenString(), bearer.getToken(), null, null, null);
final KeycloakPrincipal principal = new KeycloakPrincipal(bearer.getToken().getSubject(), session);
completeBearerAuthentication(principal, session);
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(bearer.getToken().getSubject(), session);
completeBearerAuthentication(principal);
}
}

View file

@ -53,7 +53,8 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) {
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
Set<String> roles = getRolesFromToken(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
@ -67,7 +68,8 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext securityContext) {
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = getRolesFromToken(securityContext);
for (String role : roles) {
log.info("Bearer role: " + role);

View file

@ -74,7 +74,7 @@ public class JaxrsBearerTokenFilter implements ContainerRequestFilter {
KeycloakSecurityContext skSession = new KeycloakSecurityContext(tokenString, token, null, null);
ResteasyProviderFactory.pushContext(KeycloakSecurityContext.class, skSession);
final KeycloakPrincipal principal = new KeycloakPrincipal(token.getSubject(), skSession);
final KeycloakPrincipal<KeycloakSecurityContext> principal = new KeycloakPrincipal<KeycloakSecurityContext>(token.getSubject(), skSession);
final boolean isSecure = securityContext.isSecure();
final AccessToken.Access access;
if (resourceName != null) {

View file

@ -53,7 +53,8 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal skp, RefreshableKeycloakSecurityContext securityContext) {
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> skp) {
RefreshableKeycloakSecurityContext securityContext = skp.getKeycloakSecurityContext();
request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext);
Set<String> roles = getRolesFromToken(securityContext);
GenericPrincipal principal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), skp, roles, securityContext);
@ -67,7 +68,8 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator {
}
@Override
protected void completeBearerAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext securityContext) {
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext();
Set<String> roles = getRolesFromToken(securityContext);
for (String role : roles) {
log.info("Bearer role: " + role);

View file

@ -35,18 +35,17 @@ import java.util.Set;
*/
public class KeycloakUndertowAccount implements Account, Serializable, KeycloakAccount {
protected static Logger log = Logger.getLogger(KeycloakUndertowAccount.class);
protected RefreshableKeycloakSecurityContext session;
protected KeycloakPrincipal principal;
protected KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal;
protected Set<String> accountRoles;
public KeycloakUndertowAccount(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session, KeycloakDeployment deployment) {
public KeycloakUndertowAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
this.principal = principal;
this.session = session;
setRoles(session.getToken());
setRoles(principal.getKeycloakSecurityContext().getToken());
}
protected void setRoles(AccessToken accessToken) {
Set<String> roles = null;
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
if (session.getDeployment().isUseResourceRoleMappings()) {
if (log.isTraceEnabled()) {
log.trace("useResourceRoleMappings");
@ -61,12 +60,13 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
if (access != null) roles = access.getRoles();
}
if (roles == null) roles = Collections.emptySet();
/*
log.info("Setting roles: ");
for (String role : roles) {
log.info(" role: " + role);
if (log.isTraceEnabled()) {
log.trace("Setting roles: ");
for (String role : roles) {
log.trace(" role: " + role);
}
}
*/
this.accountRoles = roles;
}
@ -82,15 +82,16 @@ public class KeycloakUndertowAccount implements Account, Serializable, KeycloakA
@Override
public RefreshableKeycloakSecurityContext getKeycloakSecurityContext() {
return session;
return principal.getKeycloakSecurityContext();
}
public void setDeployment(KeycloakDeployment deployment) {
session.setDeployment(deployment);
principal.getKeycloakSecurityContext().setDeployment(deployment);
}
public boolean isActive() {
// this object may have been serialized, so we need to reset realm config/metadata
RefreshableKeycloakSecurityContext session = getKeycloakSecurityContext();
if (session.isActive()) {
log.debug("session is active");
return true;

View file

@ -88,7 +88,7 @@ public class ServletRequestAuthenticator extends UndertowRequestAuthenticator {
}
@Override
protected KeycloakUndertowAccount createAccount(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session) {
return new KeycloakUndertowAccount(principal, session, deployment);
protected KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
return new KeycloakUndertowAccount(principal);
}
}

View file

@ -63,8 +63,8 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
}
@Override
protected void completeOAuthAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session) {
KeycloakUndertowAccount account = createAccount(principal, session);
protected void completeOAuthAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
KeycloakUndertowAccount account = createAccount(principal);
securityContext.authenticationComplete(account, "KEYCLOAK", false);
propagateKeycloakContext(account);
login(account);
@ -80,8 +80,8 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
@Override
protected void completeBearerAuthentication(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session) {
KeycloakUndertowAccount account = createAccount(principal, session);
protected void completeBearerAuthentication(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal) {
KeycloakUndertowAccount account = createAccount(principal);
securityContext.authenticationComplete(account, "KEYCLOAK", false);
propagateKeycloakContext(account);
}
@ -114,5 +114,5 @@ public abstract class UndertowRequestAuthenticator extends RequestAuthenticator
* Subclasses need to be able to create their own version of the KeycloakUndertowAccount
* @return The account
*/
protected abstract KeycloakUndertowAccount createAccount(KeycloakPrincipal principal, RefreshableKeycloakSecurityContext session);
protected abstract KeycloakUndertowAccount createAccount(KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal);
}

View file

@ -28,4 +28,7 @@ for I in *.war/WEB-INF/keycloak.json; do
sed -i -e 's/\"use-hostname-for-local-requests\": false/\"use-hostname-for-local-requests\": true/' $I;
done;
# Enable distributable for customer-portal
sed -i -e 's/<\/module-name>/&\n <distributable \/>/' customer-portal.war/WEB-INF/web.xml