[KEYCLOAK-11712] - Request body not buffered when using body CIP in Undertow

This commit is contained in:
Pedro Igor 2019-12-17 20:41:36 -03:00 committed by Stian Thorgersen
parent 709cbfd4b7
commit c596647241
11 changed files with 181 additions and 4 deletions

View file

@ -18,7 +18,9 @@
package org.keycloak.adapters.elytron; package org.keycloak.adapters.elytron;
import io.undertow.server.HttpServerExchange;
import io.undertow.server.handlers.CookieImpl; import io.undertow.server.handlers.CookieImpl;
import io.undertow.servlet.handlers.ServletRequestContext;
import org.keycloak.KeycloakSecurityContext; import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.AdapterTokenStore;
@ -38,6 +40,10 @@ import org.wildfly.security.http.Scope;
import javax.security.auth.callback.CallbackHandler; import javax.security.auth.callback.CallbackHandler;
import javax.security.cert.X509Certificate; import javax.security.cert.X509Certificate;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.ByteArrayOutputStream; import java.io.ByteArrayOutputStream;
@ -248,7 +254,26 @@ class ElytronHttpFacade implements OIDCHttpFacade {
} }
if (buffered) { if (buffered) {
return inputStream = new BufferedInputStream(request.getInputStream()); HttpScope exchangeScope = getScope(Scope.EXCHANGE);
HttpServerExchange exchange = ProtectedHttpServerExchange.class.cast(exchangeScope.getAttachment(UNDERTOW_EXCHANGE)).getExchange();
ServletRequestContext context = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletRequest servletRequest = context.getServletRequest();
inputStream = new BufferedInputStream(exchange.getInputStream());
context.setServletRequest(new HttpServletRequestWrapper((HttpServletRequest) servletRequest) {
@Override
public ServletInputStream getInputStream() {
inputStream.mark(0);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
});
return inputStream;
} }
return request.getInputStream(); return request.getInputStream();

View file

@ -23,6 +23,7 @@ import io.undertow.server.handlers.form.FormData;
import io.undertow.server.handlers.form.FormData.FormValue; import io.undertow.server.handlers.form.FormData.FormValue;
import io.undertow.server.handlers.form.FormDataParser; import io.undertow.server.handlers.form.FormDataParser;
import io.undertow.server.handlers.form.FormParserFactory; import io.undertow.server.handlers.form.FormParserFactory;
import io.undertow.servlet.handlers.ServletRequestContext;
import io.undertow.util.AttachmentKey; import io.undertow.util.AttachmentKey;
import io.undertow.util.Headers; import io.undertow.util.Headers;
import io.undertow.util.HttpString; import io.undertow.util.HttpString;
@ -32,6 +33,10 @@ import org.keycloak.adapters.spi.LogoutError;
import org.keycloak.common.util.KeycloakUriBuilder; import org.keycloak.common.util.KeycloakUriBuilder;
import javax.security.cert.X509Certificate; import javax.security.cert.X509Certificate;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.IOException; import java.io.IOException;
@ -186,7 +191,24 @@ public class UndertowHttpFacade implements HttpFacade {
} }
if (buffered) { if (buffered) {
return inputStream = new BufferedInputStream(exchange.getInputStream()); ServletRequestContext context = exchange.getAttachment(ServletRequestContext.ATTACHMENT_KEY);
ServletRequest servletRequest = context.getServletRequest();
inputStream = new BufferedInputStream(exchange.getInputStream());
context.setServletRequest(new HttpServletRequestWrapper((HttpServletRequest) servletRequest) {
@Override
public ServletInputStream getInputStream() {
inputStream.mark(0);
return new ServletInputStream() {
@Override
public int read() throws IOException {
return inputStream.read();
}
};
}
});
return inputStream;
} }
return exchange.getInputStream(); return exchange.getInputStream();

View file

@ -8,6 +8,7 @@
"credentials": { "credentials": {
"secret": "secret" "secret": "secret"
}, },
"autodetect-bearer-only": true,
"policy-enforcer": { "policy-enforcer": {
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp", "on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
"lazy-load-paths": true, "lazy-load-paths": true,
@ -19,6 +20,14 @@
"request-claim": "{request.parameter['request-claim']}" "request-claim": "{request.parameter['request-claim']}"
} }
} }
},
{
"path": "/protected/filter/body",
"claim-information-point": {
"claims": {
"request-claim": "{request.body}"
}
}
} }
] ]
} }

View file

@ -29,6 +29,11 @@
<version>${project.version}</version> <version>${project.version}</version>
<scope>provided</scope> <scope>provided</scope>
</dependency> </dependency>
<dependency>
<groupId>org.jboss.spec.javax.servlet</groupId>
<artifactId>jboss-servlet-api_4.0_spec</artifactId>
<scope>provided</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>

View file

@ -64,6 +64,10 @@
{ {
"name": "Multiple URL resource", "name": "Multiple URL resource",
"uris": ["/keycloak-7269/sub-resource1/*", "/keycloak-7269/sub-resource2/{whatever-pattern}/page.jsp"] "uris": ["/keycloak-7269/sub-resource1/*", "/keycloak-7269/sub-resource2/{whatever-pattern}/page.jsp"]
},
{
"name": "Resource Protected With Body Claim",
"uri": "/protected/filter/body"
} }
], ],
"policies": [ "policies": [
@ -221,6 +225,16 @@
"config": { "config": {
"code": "var context = $evaluation.getContext();\nvar attributes = context.getAttributes();\nvar claim = attributes.getValue('request-claim');\n\nif (claim && claim.asString(0) == 'expected-value') {\n $evaluation.grant();\n}" "code": "var context = $evaluation.getContext();\nvar attributes = context.getAttributes();\nvar claim = attributes.getValue('request-claim');\n\nif (claim && claim.asString(0) == 'expected-value') {\n $evaluation.grant();\n}"
} }
},
{
"name": "Resource Protected With Body Claim Permission",
"type": "resource",
"logic": "POSITIVE",
"decisionStrategy": "UNANIMOUS",
"config": {
"resources": "[\"Resource Protected With Body Claim\"]",
"applyPolicies": "[\"Any User Policy\"]"
}
} }
] ]
} }

View file

@ -86,6 +86,7 @@
"adminUrl": "/servlet-authz-app", "adminUrl": "/servlet-authz-app",
"bearerOnly": false, "bearerOnly": false,
"authorizationServicesEnabled": true, "authorizationServicesEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": [ "redirectUris": [
"/servlet-authz-app/*" "/servlet-authz-app/*"
], ],

View file

@ -0,0 +1,49 @@
/*
* Copyright 2019 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.servletauthz;
import javax.servlet.FilterChain;
import javax.servlet.GenericFilter;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Map;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:psilva@redhat.com">Pedro Igor</a>
*/
public class TestFilter extends GenericFilter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
if (req.getRequestURI().endsWith("/body")) {
Map body = JsonSerialization.readValue(request.getInputStream(), Map.class);
response.setContentType("application/json");
PrintWriter writer = response.getWriter();
writer.println(JsonSerialization.writeValueAsString(body));
writer.flush();
}
}
}

View file

@ -24,6 +24,16 @@
</web-resource-collection> </web-resource-collection>
</security-constraint> </security-constraint>
<filter>
<filter-name>TestFilter</filter-name>
<filter-class>org.keycloak.testsuite.servletauthz.TestFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>TestFilter</filter-name>
<url-pattern>/protected/filter/*</url-pattern>
</filter-mapping>
<login-config> <login-config>
<auth-method>KEYCLOAK</auth-method> <auth-method>KEYCLOAK</auth-method>
<realm-name>servlet-authz</realm-name> <realm-name>servlet-authz</realm-name>

View file

@ -16,13 +16,23 @@
*/ */
package org.keycloak.testsuite.adapter.example.authorization; package org.keycloak.testsuite.adapter.example.authorization;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import org.jboss.arquillian.container.test.api.Deployment; import org.jboss.arquillian.container.test.api.Deployment;
import org.jboss.shrinkwrap.api.spec.WebArchive; import org.jboss.shrinkwrap.api.spec.WebArchive;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer; import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.utils.arquillian.ContainerConstants; import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
/** /**
@ -64,4 +74,25 @@ public class ServletAuthzCIPAdapterTest extends AbstractServletAuthzAdapterTest
assertWasDenied(); assertWasDenied();
}); });
} }
@Test
public void testReuseBodyAfterClaimProcessing() {
performTests(() -> {
OAuthClient.AccessTokenResponse response = oauth.realm("servlet-authz").clientId("servlet-authz-app")
.doGrantAccessTokenRequest("secret", "alice", "alice");
Client client = ClientBuilder.newClient();
Map<String, String> body = new HashMap();
body.put("test", "test-value");
Response post = client.target(getResourceServerUrl() + "/protected/filter/body")
.request()
.header(HttpHeaders.AUTHORIZATION, "Bearer " + response.getAccessToken())
.post(Entity.entity(body, MediaType.APPLICATION_JSON_TYPE));
body = post.readEntity(Map.class);
Assert.assertEquals("test-value", body.get("test"));
});
}
} }

View file

@ -74,7 +74,7 @@ class SimpleWebXmlParser {
ElementWrapper loadOnStartupEw = servlet.getElementByTagName("load-on-startup"); ElementWrapper loadOnStartupEw = servlet.getElementByTagName("load-on-startup");
Integer loadOnStartup = loadOnStartupEw == null ? null : Integer.valueOf(loadOnStartupEw.getText()); Integer loadOnStartup = loadOnStartupEw == null ? null : Integer.valueOf(loadOnStartupEw.getText());
Class<? extends Servlet> servletClazz = (Class<? extends Servlet>) Class.forName(servletClass); Class<? extends Servlet> servletClazz = (Class<? extends Servlet>) Class.forName(servletClass, false, di.getClassLoader());
ServletInfo undertowServlet = new ServletInfo(servletName, servletClazz); ServletInfo undertowServlet = new ServletInfo(servletName, servletClazz);
if (servletMappings.containsKey(servletName)) { if (servletMappings.containsKey(servletName)) {
@ -101,7 +101,7 @@ class SimpleWebXmlParser {
String filterName = filter.getElementByTagName("filter-name").getText(); String filterName = filter.getElementByTagName("filter-name").getText();
String filterClass = filter.getElementByTagName("filter-class").getText(); String filterClass = filter.getElementByTagName("filter-class").getText();
Class<? extends Filter> filterClazz = (Class<? extends Filter>) Class.forName(filterClass); Class<? extends Filter> filterClazz = (Class<? extends Filter>) Class.forName(filterClass, false, di.getClassLoader());
FilterInfo undertowFilter = new FilterInfo(filterName, filterClazz); FilterInfo undertowFilter = new FilterInfo(filterName, filterClazz);
List<ElementWrapper> initParams = filter.getElementsByTagName("init-param"); List<ElementWrapper> initParams = filter.getElementsByTagName("init-param");

View file

@ -19,8 +19,10 @@ package org.keycloak.testsuite.utils.undertow;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import org.apache.commons.io.IOUtils;
import org.jboss.shrinkwrap.api.Archive; import org.jboss.shrinkwrap.api.Archive;
import org.jboss.shrinkwrap.api.Node; import org.jboss.shrinkwrap.api.Node;
@ -36,6 +38,15 @@ public class UndertowWarClassLoader extends ClassLoader {
this.archive = archive; this.archive = archive;
} }
@Override
protected Class<?> findClass(String name) {
try (InputStream resourceAsStream = getResourceAsStream(name.replace('.', '/') + ".class")) {
byte[] bytes = IOUtils.toByteArray(resourceAsStream);
return defineClass(name, bytes, 0, bytes.length);
} catch (IOException e) {
throw new RuntimeException("Failed to find class [" + name + "]", e);
}
}
@Override @Override
public InputStream getResourceAsStream(String name) { public InputStream getResourceAsStream(String name) {