Ensure that KeycloakUndertowAccount and referenced classes can be serialized in session
This commit is contained in:
parent
eb920b4993
commit
d7bcd41909
11 changed files with 139 additions and 43 deletions
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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: ");
|
||||
if (log.isTraceEnabled()) {
|
||||
log.trace("Setting roles: ");
|
||||
for (String role : roles) {
|
||||
log.info(" role: " + role);
|
||||
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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue