diff --git a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java index 6f1d19327e..cceb8d7770 100644 --- a/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java +++ b/adapters/oidc/wildfly-elytron/src/main/java/org/keycloak/adapters/elytron/ElytronHttpFacade.java @@ -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(); diff --git a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java index d47b363d94..c8d812b1f8 100755 --- a/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java +++ b/adapters/spi/undertow-adapter-spi/src/main/java/org/keycloak/adapters/undertow/UndertowHttpFacade.java @@ -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(); diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json index 04f09064c5..ebfe93e62f 100644 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/keycloak-claim-information-point-authz-service.json @@ -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}" + } + } } ] } diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml index 56f81cd63e..1c209d9af2 100755 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/pom.xml @@ -29,6 +29,11 @@ ${project.version} provided + + org.jboss.spec.javax.servlet + jboss-servlet-api_4.0_spec + provided + diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json index 1a1a4a7bae..873272bdc5 100644 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-app-authz-service.json @@ -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\"]" + } } ] } \ No newline at end of file diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json index 371e4510f5..5b8334de92 100644 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/servlet-authz-realm.json @@ -86,6 +86,7 @@ "adminUrl": "/servlet-authz-app", "bearerOnly": false, "authorizationServicesEnabled": true, + "directAccessGrantsEnabled": true, "redirectUris": [ "/servlet-authz-app/*" ], diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/java/org/keycloak/testsuite/servletauthz/TestFilter.java b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/java/org/keycloak/testsuite/servletauthz/TestFilter.java new file mode 100644 index 0000000000..a70c0b9de6 --- /dev/null +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/java/org/keycloak/testsuite/servletauthz/TestFilter.java @@ -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 Pedro Igor + */ +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(); + } + } +} diff --git a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml index 352b34f009..3372bbe4e2 100644 --- a/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml +++ b/testsuite/integration-arquillian/test-apps/servlet-authz/src/main/webapp/WEB-INF/web.xml @@ -23,6 +23,16 @@ /public-html.html + + + TestFilter + org.keycloak.testsuite.servletauthz.TestFilter + + + + TestFilter + /protected/filter/* + KEYCLOAK diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletAuthzCIPAdapterTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletAuthzCIPAdapterTest.java index c026399e9f..78e335b82a 100644 --- a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletAuthzCIPAdapterTest.java +++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/adapter/example/authorization/ServletAuthzCIPAdapterTest.java @@ -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 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")); + }); + } } diff --git a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/SimpleWebXmlParser.java b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/SimpleWebXmlParser.java index a77b6efcc4..96ec3d1dea 100644 --- a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/SimpleWebXmlParser.java +++ b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/SimpleWebXmlParser.java @@ -74,7 +74,7 @@ class SimpleWebXmlParser { ElementWrapper loadOnStartupEw = servlet.getElementByTagName("load-on-startup"); Integer loadOnStartup = loadOnStartupEw == null ? null : Integer.valueOf(loadOnStartupEw.getText()); - Class servletClazz = (Class) Class.forName(servletClass); + Class servletClazz = (Class) 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 filterClazz = (Class) Class.forName(filterClass); + Class filterClazz = (Class) Class.forName(filterClass, false, di.getClassLoader()); FilterInfo undertowFilter = new FilterInfo(filterName, filterClazz); List initParams = filter.getElementsByTagName("init-param"); diff --git a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/UndertowWarClassLoader.java b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/UndertowWarClassLoader.java index a36e68cb1d..4ecd7a0fd4 100644 --- a/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/UndertowWarClassLoader.java +++ b/testsuite/integration-arquillian/util/src/main/java/org/keycloak/testsuite/utils/undertow/UndertowWarClassLoader.java @@ -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) {