[KEYCLOAK-11712] - Request body not buffered when using body CIP in Undertow
This commit is contained in:
parent
709cbfd4b7
commit
c596647241
11 changed files with 181 additions and 4 deletions
|
@ -18,7 +18,9 @@
|
|||
|
||||
package org.keycloak.adapters.elytron;
|
||||
|
||||
import io.undertow.server.HttpServerExchange;
|
||||
import io.undertow.server.handlers.CookieImpl;
|
||||
import io.undertow.servlet.handlers.ServletRequestContext;
|
||||
import org.keycloak.KeycloakSecurityContext;
|
||||
import org.keycloak.adapters.AdapterDeploymentContext;
|
||||
import org.keycloak.adapters.AdapterTokenStore;
|
||||
|
@ -38,6 +40,10 @@ import org.wildfly.security.http.Scope;
|
|||
|
||||
import javax.security.auth.callback.CallbackHandler;
|
||||
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.ByteArrayOutputStream;
|
||||
|
@ -248,7 +254,26 @@ class ElytronHttpFacade implements OIDCHttpFacade {
|
|||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -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.FormDataParser;
|
||||
import io.undertow.server.handlers.form.FormParserFactory;
|
||||
import io.undertow.servlet.handlers.ServletRequestContext;
|
||||
import io.undertow.util.AttachmentKey;
|
||||
import io.undertow.util.Headers;
|
||||
import io.undertow.util.HttpString;
|
||||
|
@ -32,6 +33,10 @@ import org.keycloak.adapters.spi.LogoutError;
|
|||
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||
|
||||
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.IOException;
|
||||
|
@ -186,7 +191,24 @@ public class UndertowHttpFacade implements HttpFacade {
|
|||
}
|
||||
|
||||
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();
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
"credentials": {
|
||||
"secret": "secret"
|
||||
},
|
||||
"autodetect-bearer-only": true,
|
||||
"policy-enforcer": {
|
||||
"on-deny-redirect-to" : "/servlet-authz-app/accessDenied.jsp",
|
||||
"lazy-load-paths": true,
|
||||
|
@ -19,6 +20,14 @@
|
|||
"request-claim": "{request.parameter['request-claim']}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "/protected/filter/body",
|
||||
"claim-information-point": {
|
||||
"claims": {
|
||||
"request-claim": "{request.body}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -29,6 +29,11 @@
|
|||
<version>${project.version}</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jboss.spec.javax.servlet</groupId>
|
||||
<artifactId>jboss-servlet-api_4.0_spec</artifactId>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -64,6 +64,10 @@
|
|||
{
|
||||
"name": "Multiple URL resource",
|
||||
"uris": ["/keycloak-7269/sub-resource1/*", "/keycloak-7269/sub-resource2/{whatever-pattern}/page.jsp"]
|
||||
},
|
||||
{
|
||||
"name": "Resource Protected With Body Claim",
|
||||
"uri": "/protected/filter/body"
|
||||
}
|
||||
],
|
||||
"policies": [
|
||||
|
@ -221,6 +225,16 @@
|
|||
"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}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Resource Protected With Body Claim Permission",
|
||||
"type": "resource",
|
||||
"logic": "POSITIVE",
|
||||
"decisionStrategy": "UNANIMOUS",
|
||||
"config": {
|
||||
"resources": "[\"Resource Protected With Body Claim\"]",
|
||||
"applyPolicies": "[\"Any User Policy\"]"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
|
@ -86,6 +86,7 @@
|
|||
"adminUrl": "/servlet-authz-app",
|
||||
"bearerOnly": false,
|
||||
"authorizationServicesEnabled": true,
|
||||
"directAccessGrantsEnabled": true,
|
||||
"redirectUris": [
|
||||
"/servlet-authz-app/*"
|
||||
],
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -24,6 +24,16 @@
|
|||
</web-resource-collection>
|
||||
</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>
|
||||
<auth-method>KEYCLOAK</auth-method>
|
||||
<realm-name>servlet-authz</realm-name>
|
||||
|
|
|
@ -16,13 +16,23 @@
|
|||
*/
|
||||
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.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.jboss.arquillian.container.test.api.Deployment;
|
||||
import org.jboss.shrinkwrap.api.spec.WebArchive;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.keycloak.testsuite.arquillian.annotation.AppServerContainer;
|
||||
import org.keycloak.testsuite.util.OAuthClient;
|
||||
import org.keycloak.testsuite.utils.arquillian.ContainerConstants;
|
||||
|
||||
/**
|
||||
|
@ -64,4 +74,25 @@ public class ServletAuthzCIPAdapterTest extends AbstractServletAuthzAdapterTest
|
|||
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"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ class SimpleWebXmlParser {
|
|||
ElementWrapper loadOnStartupEw = servlet.getElementByTagName("load-on-startup");
|
||||
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);
|
||||
|
||||
if (servletMappings.containsKey(servletName)) {
|
||||
|
@ -101,7 +101,7 @@ class SimpleWebXmlParser {
|
|||
String filterName = filter.getElementByTagName("filter-name").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);
|
||||
|
||||
List<ElementWrapper> initParams = filter.getElementsByTagName("init-param");
|
||||
|
|
|
@ -19,8 +19,10 @@ package org.keycloak.testsuite.utils.undertow;
|
|||
|
||||
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.jboss.shrinkwrap.api.Archive;
|
||||
import org.jboss.shrinkwrap.api.Node;
|
||||
|
||||
|
@ -36,6 +38,15 @@ public class UndertowWarClassLoader extends ClassLoader {
|
|||
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
|
||||
public InputStream getResourceAsStream(String name) {
|
||||
|
|
Loading…
Reference in a new issue