[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;
|
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();
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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\"]"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
|
@ -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/*"
|
||||||
],
|
],
|
||||||
|
|
|
@ -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>
|
</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>
|
||||||
|
|
|
@ -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"));
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in a new issue