Internal class that wraps the original request and allows the replay + * of the input stream and parsed form params.
+ * + * @author rmartinc + */ +class ReplayHttpServletRequestWrapper extends HttpServletRequestWrapper { + + private final ReplayServletInputStream ris; + private final FormData formData; + private ListInternal class that allows the replay of the InputStream using the + * direct bytes.
+ * + * @author rmartinc + */ +class ReplayServletInputStream extends ServletInputStream { + + private final byte[] bytes; + private int idx; + private ReadListener listener = null; + + public ReplayServletInputStream(byte[] bytes) { + this.bytes = bytes; + this.idx = -1; + } + + @Override + public boolean isFinished() { + return idx >= bytes.length - 1; + } + + @Override + public boolean isReady() { + return !isFinished(); + } + + @Override + public void setReadListener(ReadListener listener) { + this.listener = listener; + if (isReady()) { + try { + listener.onDataAvailable(); + } catch (IOException e) { + listener.onError(e); + } + } else { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } + } + + @Override + public int read() throws IOException { + int result = -1; + if (isReady()) { + result = bytes[++idx]; + if (isFinished() && listener != null) { + try { + listener.onAllDataRead(); + } catch (IOException e) { + listener.onError(e); + } + } + } + return result; + } + +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java new file mode 100644 index 00000000..ecabfc9b --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/CustomFormServletAuthenticationTest.java @@ -0,0 +1,362 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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.wildfly.elytron.web.undertow.server.servlet; + +import io.undertow.UndertowOptions; +import java.io.File; +import java.net.URI; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.HttpMultipartMode; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.entity.mime.content.FileBody; +import org.apache.http.entity.mime.content.StringBody; +import org.apache.http.impl.client.HttpClientBuilder; +import org.apache.http.message.BasicNameValuePair; +import org.apache.http.util.EntityUtils; +import org.hamcrest.CoreMatchers; +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TemporaryFolder; +import org.wildfly.elytron.web.undertow.common.AbstractHttpServerMechanismTest; +import org.wildfly.elytron.web.undertow.common.UndertowServer; +import org.wildfly.elytron.web.undertow.server.servlet.util.CustomFormParamHttpAuthenticationMechanism; +import org.wildfly.elytron.web.undertow.server.servlet.util.CustomFormParamMechanismFactory; +import org.wildfly.elytron.web.undertow.server.servlet.util.UndertowServletServer; +import org.wildfly.security.auth.permission.LoginPermission; +import org.wildfly.security.auth.realm.SimpleMapBackedSecurityRealm; +import org.wildfly.security.auth.realm.SimpleRealmEntry; +import org.wildfly.security.auth.server.SecurityDomain; +import org.wildfly.security.credential.PasswordCredential; +import org.wildfly.security.http.util.FilterServerMechanismFactory; +import org.wildfly.security.http.util.PropertiesServerMechanismFactory; +import org.wildfly.security.password.PasswordFactory; +import org.wildfly.security.password.interfaces.ClearPassword; +import org.wildfly.security.password.spec.ClearPasswordSpec; +import org.wildfly.security.permission.PermissionVerifier; + +/** + *Test that uses a custom form mechanism in order to check that + * parameters and the input stream are available after parsing and can be + * replayed.
+ * + * @author rmartinc + */ +public class CustomFormServletAuthenticationTest extends AbstractHttpServerMechanismTest { + + @Rule + public UndertowServer server = createUndertowServer(); + + @Rule + public TemporaryFolder folder = new TemporaryFolder(); + + public CustomFormServletAuthenticationTest() throws Exception { + } + + @Test + public void testLogin() throws Exception { + HttpClient httpClient = HttpClientBuilder.create().build(); + HttpPost httpAuthenticate = new HttpPost(server.createUri("/secured")); + ListCustom Form mechanism. It uses two form parameters to obtain the + * username and password (X-USERNAME and X-PASSWORD). It is used to test that + * replay is done OK.
+ * + * @author rmartinc + */ +public class CustomFormParamHttpAuthenticationMechanism implements HttpServerAuthenticationMechanism { + + public static final String USERNAME_PARAM = "X-USERNAME"; + public static final String PASSWORD_PARAM = "X-PASSWORD"; + public static final String MESSAGE_HEADER = "X-MESSAGE"; + + private static final HttpServerMechanismsResponder RESPONDER = new HttpServerMechanismsResponder() { + @Override + public void sendResponse(HttpServerResponse response) throws HttpAuthenticationException { + response.addResponseHeader(MESSAGE_HEADER, "Please resubmit the request with a username specified using the X-USERNAME and a password specified using the X-PASSWORD form attributes."); + response.setStatusCode(401); + } + }; + + private final CallbackHandler callbackHandler; + + CustomFormParamHttpAuthenticationMechanism(final CallbackHandler callbackHandler) { + this.callbackHandler = callbackHandler; + } + + @Override + public void evaluateRequest(HttpServerRequest request) throws HttpAuthenticationException { + final String username = request.getFirstParameterValue(USERNAME_PARAM); + final String password = request.getFirstParameterValue(PASSWORD_PARAM); + + if (username == null || username.length() == 0 || password == null || password.length() == 0) { + request.noAuthenticationInProgress(RESPONDER); + return; + } + + NameCallback nameCallback = new NameCallback("Remote Authentication Name", username); + nameCallback.setName(username); + final PasswordGuessEvidence evidence = new PasswordGuessEvidence(password.toCharArray()); + EvidenceVerifyCallback evidenceVerifyCallback = new EvidenceVerifyCallback(evidence); + + try { + callbackHandler.handle(new Callback[] { nameCallback, evidenceVerifyCallback }); + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + + if (evidenceVerifyCallback.isVerified() == false) { + request.authenticationFailed("Username / Password Validation Failed", RESPONDER); + return; + } + + try { + callbackHandler.handle(new Callback[] {new IdentityCredentialCallback(new PasswordCredential(ClearPassword.createRaw(ClearPassword.ALGORITHM_CLEAR, password.toCharArray())), true)}); + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + + AuthorizeCallback authorizeCallback = new AuthorizeCallback(username, username); + + try { + callbackHandler.handle(new Callback[] {authorizeCallback}); + + if (authorizeCallback.isAuthorized()) { + callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.SUCCEEDED }); + request.authenticationComplete(); + } else { + callbackHandler.handle(new Callback[] { AuthenticationCompleteCallback.FAILED }); + request.authenticationFailed("Authorization check failed.", RESPONDER); + } + } catch (IOException | UnsupportedCallbackException e) { + throw new HttpAuthenticationException(e); + } + } + + @Override + public String getMechanismName() { + return CustomFormParamMechanismFactory.CUSTOM_NAME; + } +} \ No newline at end of file diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java new file mode 100644 index 00000000..3e30fc93 --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/CustomFormParamMechanismFactory.java @@ -0,0 +1,50 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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.wildfly.elytron.web.undertow.server.servlet.util; + +import java.util.Map; + +import javax.security.auth.callback.CallbackHandler; + +import org.wildfly.security.http.HttpAuthenticationException; +import org.wildfly.security.http.HttpServerAuthenticationMechanism; +import org.wildfly.security.http.HttpServerAuthenticationMechanismFactory; + +/** + *Form mechanism factory.
+ * + * @author rmartinc + */ +public class CustomFormParamMechanismFactory implements HttpServerAuthenticationMechanismFactory { + + public static final String CUSTOM_NAME = "CUSTOM_FORM_MECHANISM"; + + @Override + public HttpServerAuthenticationMechanism createAuthenticationMechanism(String name, MapA Servlet that gets the input stream and copies it back to the output.
+ * + * @author rmartinc + */ +public class InputStreamServlet extends HttpServlet { + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { + TestServlet.manageLoginHeaders(req, resp); + resp.setContentType("text/plain;charset=UTF-8"); + try (OutputStream out = resp.getOutputStream(); + InputStream in = req.getInputStream()) { + byte[] buf = new byte[512]; + int length; + while ((length = in.read(buf)) != -1) { + out.write(buf, 0, length); + } + } + } + +} diff --git a/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java new file mode 100644 index 00000000..5a28102d --- /dev/null +++ b/undertow-servlet/src/test/java/org/wildfly/elytron/web/undertow/server/servlet/util/MultiPartServlet.java @@ -0,0 +1,81 @@ +/* + * Copyright 2022 JBoss by Red Hat. + * + * 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.wildfly.elytron.web.undertow.server.servlet.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import jakarta.servlet.http.Part; + +/** + *A MultiPartServlet that displays information for common parameters and + * multi parts. Same op parameter is used:
+ * + *A Servlet that displays information for common parameters in the request. + * The op parameter can be used to test a different method:
+ * + *