KEYCLOAK-14871 Whitelist RefreshableKeycloakSecurityContext for KeycloakPrincipal serialization

This commit is contained in:
mhajas 2020-09-24 11:06:35 +02:00 committed by Hynek Mlnařík
parent 086f7b4696
commit d266165f63
8 changed files with 341 additions and 0 deletions

View file

@ -1,8 +1,21 @@
package org.keycloak.adapters;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.jose.jws.JWSBuilder;
import org.keycloak.representations.AccessToken;
import org.keycloak.representations.IDToken;
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
@ -39,4 +52,63 @@ public class RefreshableKeycloakSecurityContextTest {
token.issuedAt(5000);
assertTrue(sut.isActive());
}
private AccessToken createSimpleToken() {
AccessToken token = new AccessToken();
token.id("111");
token.issuer("http://localhost:8080/auth/acme");
token.addAccess("foo").addRole("admin");
token.addAccess("bar").addRole("user");
return token;
}
@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());
KeycloakDeployment keycloakDeployment = new KeycloakDeployment();
keycloakDeployment.setNotBefore(5000);
KeycloakSecurityContext ctx = new RefreshableKeycloakSecurityContext(keycloakDeployment,null, encoded, token,encodedIdToken, null, null);
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());
Assert.assertEquals("acme", ctx.getRealm());
ois.close();
}
}

View file

@ -72,6 +72,7 @@ public class KeycloakPrincipal<T extends KeycloakSecurityContext> implements Pri
DelegatingSerializationFilter.builder()
.addAllowedClass(KeycloakPrincipal.class)
.addAllowedClass(KeycloakSecurityContext.class)
.addAllowedPattern("org.keycloak.adapters.RefreshableKeycloakSecurityContext")
.setFilter(in);
in.defaultReadObject();

View file

@ -0,0 +1,82 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other 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.testsuite.adapter.servlet;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.ObjectStreamClass;
import java.io.PrintWriter;
/**
* @author mhajas
*/
@WebServlet("/serialization-servlet")
public class SerializationServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
PrintWriter pw = resp.getWriter();
// Serialize
ByteArrayOutputStream bso = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bso);
oos.writeObject(req.getUserPrincipal());
oos.close();
// Deserialize
byte[] bytes = bso.toByteArray();
ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bis) {
@Override
public Class resolveClass(ObjectStreamClass desc) throws IOException, ClassNotFoundException {
try {
return Class.forName(desc.getName(), true, SerializationServlet.class.getClassLoader());
} catch (Exception e) { }
// Fall back (e.g. for primClasses)
return super.resolveClass(desc);
}
};
KeycloakPrincipal principal;
try {
principal = (KeycloakPrincipal) ois.readObject();
} catch (ClassNotFoundException e) {
e.printStackTrace();
pw.write("Deserialization failed");
return;
}
KeycloakSecurityContext ctx = principal.getKeycloakSecurityContext();
if (!(ctx instanceof RefreshableKeycloakSecurityContext)) {
pw.write("Context was not instance of RefreshableKeycloakSecurityContext");
}
pw.write("Serialization/Deserialization was successful");
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2016 Red Hat, Inc. and/or its affiliates
* and other 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.testsuite.adapter.page;
import org.jboss.arquillian.container.test.api.OperateOnDeployment;
import org.jboss.arquillian.test.api.ArquillianResource;
import org.keycloak.testsuite.page.AbstractPageWithInjectedUrl;
import javax.ws.rs.core.UriBuilder;
import java.net.URI;
import java.net.URL;
/**
*
* @author tkyjovsk
*/
public class SerializationServletPage extends AbstractPageWithInjectedUrl {
public static final String DEPLOYMENT_NAME = "serialization-servlet";
@ArquillianResource
@OperateOnDeployment(DEPLOYMENT_NAME)
private URL url;
@Override
public URL getInjectedUrl() {
return url;
}
public URI logout() {
return getUriBuilder().clone().path("logout").build();
}
}

View file

@ -0,0 +1,41 @@
package org.keycloak.testsuite.adapter.servlet;
import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.arquillian.graphene.page.Page;
import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Test;
import org.keycloak.testsuite.adapter.AbstractServletsAdapterTest;
import org.keycloak.testsuite.adapter.page.SerializationServletPage;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.not;
import static org.junit.Assert.assertThat;
@AppServerContainer(ContainerConstants.APP_SERVER_UNDERTOW)
@AppServerContainer(ContainerConstants.APP_SERVER_WILDFLY)
@AppServerContainer(ContainerConstants.APP_SERVER_EAP)
// The purpose of this class is to test KeycloakPrincipal serialization on different app-server-jdks
public class KeycloakPrincipalSerializationTest extends AbstractServletsAdapterTest {
@Page
protected SerializationServletPage serializationServlet;
@Deployment(name = SerializationServletPage.DEPLOYMENT_NAME)
protected static WebArchive serializationServlet() {
return servletDeployment(SerializationServletPage.DEPLOYMENT_NAME, SerializationServlet.class, ErrorServlet.class, ServletTestUtils.class);
}
@Test
public void testKeycloakPrincipalSerialization() {
serializationServlet.navigateTo();
testRealmLoginPage.form().login("bburke@redhat.com", "password");
assertThat(driver.getPageSource(), containsString("Serialization/Deserialization was successful"));
assertThat(driver.getPageSource(), not(containsString("Context was not instance of RefreshableKeycloakSecurityContext")));
assertThat(driver.getPageSource(), not(containsString("Deserialization failed")));
}
}

View file

@ -151,6 +151,17 @@
"secret": "password",
"directAccessGrantsEnabled": true
},
{
"clientId": "serialization-servlet",
"enabled": true,
"adminUrl": "/serialization-servlet",
"baseUrl": "/serialization-servlet",
"redirectUris": [
"/serialization-servlet/*"
],
"secret": "password",
"directAccessGrantsEnabled": true
},
{
"clientId": "customer-portal-subsystem",
"enabled": true,

View file

@ -0,0 +1,10 @@
{
"realm": "demo",
"resource": "serialization-servlet",
"auth-server-url": "http://localhost:8180/auth",
"ssl-required" : "external",
"expose-token": true,
"credentials": {
"secret": "password"
}
}

View file

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Copyright 2016 Red Hat, Inc. and/or its affiliates
~ and other 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.
-->
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<module-name>serialization-servlet</module-name>
<servlet>
<servlet-name>Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.SerializationServlet</servlet-class>
</servlet>
<servlet>
<servlet-name>Error Servlet</servlet-name>
<servlet-class>org.keycloak.testsuite.adapter.servlet.ErrorServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>Servlet</servlet-name>
<url-pattern>/*</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>Error Servlet</servlet-name>
<url-pattern>/error.html</url-pattern>
</servlet-mapping>
<security-constraint>
<web-resource-collection>
<web-resource-name>Users</web-resource-name>
<url-pattern>/*</url-pattern>
</web-resource-collection>
<auth-constraint>
<role-name>user</role-name>
</auth-constraint>
</security-constraint>
<security-constraint>
<web-resource-collection>
<web-resource-name>Errors</web-resource-name>
<url-pattern>/error.html</url-pattern>
</web-resource-collection>
</security-constraint>
<login-config>
<auth-method>KEYCLOAK</auth-method>
<realm-name>demo</realm-name>
<form-login-config>
<form-login-page>/error.html</form-login-page>
<form-error-page>/error.html</form-error-page>
</form-login-config>
</login-config>
<security-role>
<role-name>admin</role-name>
</security-role>
<security-role>
<role-name>user</role-name>
</security-role>
</web-app>