This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.auth.form-1.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-form.git
commit a0fcd1a92f27be07f64d8e1fa3263b2e58c17d1b Author: Felix Meschberger <[email protected]> AuthorDate: Tue Feb 9 10:24:13 2010 +0000 SLING-1116 Implement support for the j_validate login form parameter and add support to convey a reason to render the login form using the j_reason request parameter for the login form request git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/formauth@907990 13f79535-47bb-0310-9956-ffa450edef68 --- .../sling/formauth/AuthenticationFormServlet.java | 101 ++++++-- .../sling/formauth/FormAuthenticationHandler.java | 274 +++++++++++++++------ .../java/org/apache/sling/formauth/FormReason.java | 50 ++++ .../{ => org/apache/sling/formauth}/login.html | 32 ++- .../org/apache/sling/formauth/FormReasonTest.java | 43 ++++ 5 files changed, 410 insertions(+), 90 deletions(-) diff --git a/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java b/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java index 7c9f789..d08016a 100644 --- a/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java +++ b/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java @@ -23,13 +23,10 @@ import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; -import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.apache.sling.commons.auth.Authenticator; - /** * The <code>AuthenticationFormServlet</code> provides the default login form * used for Form Based Authentication. @@ -44,27 +41,54 @@ import org.apache.sling.commons.auth.Authenticator; public class AuthenticationFormServlet extends HttpServlet { /** + * The constant is sued to provide the service registration path + * * @scr.property name="sling.servlet.paths" */ static final String SERVLET_PATH = "/system/sling/form/login"; /** + * This constant is used to provide the service registration property + * indicating to pass requests to this servlet unauthenticated. + * * @scr.property name="sling.auth.requirements" */ @SuppressWarnings("unused") private static final String AUTH_REQUIREMENT = "-" + SERVLET_PATH; + /** + * The raw form used by the {@link #getForm(HttpServletRequest)} method to + * fill in with per-request data. This field is set by the + * {@link #getRawForm()} method when first loading the form. + */ private volatile String rawForm; + /** + * Prepares and returns the login form. The response is sent as an UTF-8 + * encoded <code>text/html</code> page with all known cache control headers + * set to prevent all caching. + * <p> + * This servlet is to be called to handle the request directly, that is it + * expected to not be included and for the response to not be committed yet + * because it first resets the response. + * + * @throws IOException if an error occurrs preparing or sending back the + * login form + * @throws IllegalStateException if the response has already been committed + * and thus response reset is not possible. + */ @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { + // reset the response first + response.reset(); + // setup the response for HTML and cache prevention response.setContentType("text/html"); response.setCharacterEncoding("UTF-8"); - response.setHeader("Cache-control", "no-cache"); - response.addHeader("Cache-control", "no-store"); + response.setHeader("Cache-Control", "no-cache"); + response.addHeader("Cache-Control", "no-store"); response.setHeader("Pragma", "no-cache"); response.setHeader("Expires", "0"); @@ -73,30 +97,73 @@ public class AuthenticationFormServlet extends HttpServlet { response.flushBuffer(); } - @Override - protected void doPost(HttpServletRequest request, - HttpServletResponse response) throws ServletException, IOException { - super.doPost(request, response); + /** + * Returns the form to be sent back to the client for login providing an + * optional informational message and the optional target to redirect to + * after successfully logging in. + * + * @param request The request providing parameters indicating the + * informational message and redirection target. + * @return The login form to be returned to the client + * @throws IOException If the login form cannot be loaded + */ + private String getForm(final HttpServletRequest request) throws IOException { + String form = getRawForm(); + + form = form.replace("${resource}", getResource(request)); + form = form.replace("${j_reason}", getReason(request)); + + return form; } - private String getForm(final HttpServletRequest request) throws IOException { + /** + * Returns the path to the resource to which the request should be + * redirected after successfully completing the form or an empty string if + * there is no <code>resource</code> request parameter. + * + * @param request The request providing the <code>resource</code> parameter. + * @return The target to redirect after sucessfully login or an empty string + * if no specific target has been requested. + */ + private String getResource(final HttpServletRequest request) { + final String resource = FormAuthenticationHandler.getLoginResource(request); + return (resource == null) ? "" : resource; + } - String resource = (String) request.getAttribute(Authenticator.LOGIN_RESOURCE); - if (resource == null) { - resource = request.getParameter(Authenticator.LOGIN_RESOURCE); - if (resource == null) { - resource = "/"; + /** + * Returns an informational message according to the value provided in the + * <code>j_reason</code> request parameter. Supported reasons are invalid + * credentials and session timeout. + * + * @param request The request providing the parameter + * @return The "translated" reason to render the login form or an empty + * string if there is no specific reason + */ + private String getReason(final HttpServletRequest request) { + final String reason = request.getParameter(FormAuthenticationHandler.PAR_J_REASON); + if (reason != null) { + try { + return FormReason.valueOf(reason).getMessage(); + } catch (IllegalArgumentException iae) { + // thrown if the reason is not an expected value, assume none } } - return getRawForm().replace("${resource}", resource); + return ""; } + /** + * Load the raw unmodified form from the bundle (through the class loader). + * + * @return The raw form as a string + * @throws IOException If an error occurrs reading the "file" or if the + * class loader cannot provide the form data. + */ private String getRawForm() throws IOException { if (rawForm == null) { InputStream ins = null; try { - ins = getClass().getResourceAsStream("/login.html"); + ins = getClass().getResourceAsStream("login.html"); if (ins != null) { StringBuilder builder = new StringBuilder(); Reader r = new InputStreamReader(ins, "UTF-8"); diff --git a/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java b/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java index c135cc4..9dc5b30 100644 --- a/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java +++ b/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java @@ -190,11 +190,26 @@ public class FormAuthenticationHandler implements AuthenticationHandler, private static final String PAR_J_PASSWORD = "j_password"; /** + * The name of the form submission parameter indicating that the submitted + * username and password should just be checked and a status code be set for + * success (200/OK) or failure (403/FORBIDDEN). + */ + private static final String PAR_J_VALIDATE = "j_validate"; + + /** + * The name of the request parameter indicating to the login form why the + * form is being rendered. If this parameter is not set the form is called + * for the first time and the implied reason is that the authenticator just + * requests credentials. Otherwise the parameter is set to a + * {@link FormReason} value. + */ + static final String PAR_J_REASON = "j_reason"; + + /** * The factor to convert minute numbers into milliseconds used internally */ private static final long MINUTES = 60L * 1000L; - /** default log */ private final Logger log = LoggerFactory.getLogger(getClass()); @@ -231,43 +246,75 @@ public class FormAuthenticationHandler implements AuthenticationHandler, // 2. try credentials from the cookie or session if (info == null) { String authData = authStorage.extractAuthenticationInfo(request); - if (authData != null && tokenStore.isValid(authData)) { - info = createAuthInfo(authData); + if (authData != null) { + if (tokenStore.isValid(authData)) { + info = createAuthInfo(authData); + } else { + // signal the requestCredentials method a previous login failure + request.setAttribute(PAR_J_REASON, FormReason.TIMEOUT); + } } } return info; } - /* - * (non-Javadoc) - * @see - * org.apache.sling.commons.auth.spi.AuthenticationHandler#requestCredentials - * (javax.servlet.http.HttpServletRequest, - * javax.servlet.http.HttpServletResponse) + /** + * Unless the <code>sling:authRequestLogin</code> to anything other than + * <code>Form</code> this method either sends back a 403/FORBIDDEN response + * if the <code>j_verify</code> parameter is set to <code>true</code> or + * redirects to the login form to ask for credentials. + * <p> + * This method assumes the <code>j_verify</code> request parameter to only + * be set in the initial username/password submission through the login + * form. No further checks are applied, though, before sending back the + * 403/FORBIDDEN response. */ public boolean requestCredentials(HttpServletRequest request, HttpServletResponse response) throws IOException { // 0. ignore this handler if an authentication handler is requested if (ignoreRequestCredentials(request)) { + // consider this handler is not used return false; } - String resource = (String) request.getAttribute(Authenticator.LOGIN_RESOURCE); - if (resource == null || resource.length() == 0) { - resource = request.getParameter(Authenticator.LOGIN_RESOURCE); - if (resource == null || resource.length() == 0) { - resource = request.getRequestURI(); + // 1. check whether we short cut for a failed log in with validation + if (isValidateRequest(request)) { + try { + response.setStatus(403); + response.flushBuffer(); + } catch (IOException ioe) { + log.error("Failed to send 403/FORBIDDEN response", ioe); } + + // consider credentials requested + return true; } + // prepare the login form redirection target final StringBuilder targetBuilder = new StringBuilder(); targetBuilder.append(request.getContextPath()); targetBuilder.append(loginForm); - targetBuilder.append('?').append(Authenticator.LOGIN_RESOURCE); - targetBuilder.append("=").append(URLEncoder.encode(resource, "UTF-8")); + // append originally requested resource (for redirect after login) + char parSep = '?'; + final String resource = getLoginResource(request); + if (resource != null) { + targetBuilder.append(parSep).append(Authenticator.LOGIN_RESOURCE); + targetBuilder.append("=").append( + URLEncoder.encode(resource, "UTF-8")); + parSep = '&'; + } + + // append indication of previous login failure + if (request.getAttribute(PAR_J_REASON) != null) { + final String reason = String.valueOf(request.getAttribute(PAR_J_REASON)); + targetBuilder.append(parSep).append(PAR_J_REASON); + targetBuilder.append("=").append(URLEncoder.encode(reason, "UTF-8")); + } + + // finally redirect to the login form final String target = targetBuilder.toString(); try { response.sendRedirect(target); @@ -278,30 +325,13 @@ public class FormAuthenticationHandler implements AuthenticationHandler, return true; } - /* - * (non-Javadoc) - * @see - * org.apache.sling.commons.auth.spi.AuthenticationHandler#dropCredentials - * (javax.servlet.http.HttpServletRequest, - * javax.servlet.http.HttpServletResponse) + /** + * Clears all authentication state which might have been prepared by this + * authentication handler. */ public void dropCredentials(HttpServletRequest request, HttpServletResponse response) { - authStorage.clear(request, response); - - // if there is a referer header, redirect back there - // with an anonymous session - String referer = request.getHeader("referer"); - if (referer == null) { - referer = request.getContextPath() + "/"; - } - - try { - response.sendRedirect(referer); - } catch (IOException e) { - log.error("Failed to redirect to the page: " + referer, e); - } } // ---------- AuthenticationFeedbackHandler @@ -313,7 +343,17 @@ public class FormAuthenticationHandler implements AuthenticationHandler, */ public void authenticationFailed(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) { + + /* + * Note: This method is called if this handler provided credentials + * which cause a login failure + */ + + // clear authentication data from Cookie or Http Session authStorage.clear(request, response); + + // signal the requestCredentials method a previous login failure + request.setAttribute(PAR_J_REASON, FormReason.INVALID_CREDENTIALS); } /** @@ -332,55 +372,56 @@ public class FormAuthenticationHandler implements AuthenticationHandler, public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) { - // get current authentication data, may be missing after first login - String authData = getCookieAuthData(authInfo.getCredentials()); + /* + * Note: This method is called if this handler provided credentials + * which succeeded loging into the repository + */ - // check whether we have to "store" or create the data - final boolean refreshCookie = needsRefresh(authData, - this.sessionTimeout); + // ensure fresh authentication data + refreshAuthData(request, response, authInfo); + + final boolean result; + if (isValidateRequest(request)) { - // add or refresh the stored auth hash - if (refreshCookie) { - long expires = System.currentTimeMillis() + this.sessionTimeout; try { - authData = null; - authData = tokenStore.encode(expires, authInfo.getUser()); - } catch (InvalidKeyException e) { - log.error(e.getMessage(), e); - } catch (IllegalStateException e) { - log.error(e.getMessage(), e); - } catch (UnsupportedEncodingException e) { - log.error(e.getMessage(), e); - } catch (NoSuchAlgorithmException e) { - log.error(e.getMessage(), e); + response.setStatus(200); + response.flushBuffer(); + } catch (IOException ioe) { + log.error("Failed to send 200/OK response", ioe); } - if (authData != null) { - authStorage.set(request, response, authData); - } else { - authStorage.clear(request, response); - } - } + // terminate request, all done + result = true; - if (!DefaultAuthenticationFeedbackHandler.handleRedirect(request, - response)) { + } else if (DefaultAuthenticationFeedbackHandler.handleRedirect( + request, response)) { - String resource = (String) request.getAttribute(Authenticator.LOGIN_RESOURCE); - if (resource == null || resource.length() == 0) { - resource = request.getParameter(Authenticator.LOGIN_RESOURCE); - } - if (resource != null && resource.length() > 0) { + // terminate request, all done in the default handler + result = false; + + } else { + + // check whether redirect is requested by the resource parameter + + final String resource = getLoginResource(request); + if (resource != null) { try { response.sendRedirect(resource); } catch (IOException ioe) { + log.error("Failed to send redirect to: " + resource, ioe); } - return true; + + // terminate request, all done + result = true; + } else { + // no redirect, hence continue processing + result = false; } } // no redirect - return false; + return result; } @Override @@ -399,12 +440,103 @@ public class FormAuthenticationHandler implements AuthenticationHandler, * {@link #REQUEST_LOGIN_PARAMETER} is set to any value other than "Form" * (HttpServletRequest.FORM_AUTH). */ - private boolean ignoreRequestCredentials(HttpServletRequest request) { + private boolean ignoreRequestCredentials(final HttpServletRequest request) { final String requestLogin = request.getParameter(REQUEST_LOGIN_PARAMETER); return requestLogin != null && !HttpServletRequest.FORM_AUTH.equals(requestLogin); } + /** + * Returns <code>true</code> if the the client just asks for validation of + * submitted username/password credentials. + * <p> + * This implementation returns <code>true</code> if the request parameter + * {@link #PAR_J_VALIDATE} is set to <code>true</code> (case-insensitve). If + * the request parameter is not set or to any value other than + * <code>true</code> this method returns <code>false</code>. + * + * @param request The request to provide the parameter to check + * @return <code>true</code> if the {@link #PAR_J_VALIDATE} parameter is set + * to <code>true</code>. + */ + private boolean isValidateRequest(final HttpServletRequest request) { + return "true".equalsIgnoreCase(request.getParameter(PAR_J_VALIDATE)); + } + + /** + * Ensures the authentication data is set (if not set yet) and the expiry + * time is prolonged (if auth data already existed). + * <p> + * This method is intended to be called in case authentication succeeded. + * + * @param request The curent request + * @param response The current response + * @param authInfo The authentication info used to successfull log in + */ + private void refreshAuthData(final HttpServletRequest request, + final HttpServletResponse response, + final AuthenticationInfo authInfo) { + + // get current authentication data, may be missing after first login + String authData = getCookieAuthData(authInfo.getCredentials()); + + // check whether we have to "store" or create the data + final boolean refreshCookie = needsRefresh(authData, + this.sessionTimeout); + + // add or refresh the stored auth hash + if (refreshCookie) { + long expires = System.currentTimeMillis() + this.sessionTimeout; + try { + authData = null; + authData = tokenStore.encode(expires, authInfo.getUser()); + } catch (InvalidKeyException e) { + log.error(e.getMessage(), e); + } catch (IllegalStateException e) { + log.error(e.getMessage(), e); + } catch (UnsupportedEncodingException e) { + log.error(e.getMessage(), e); + } catch (NoSuchAlgorithmException e) { + log.error(e.getMessage(), e); + } + + if (authData != null) { + authStorage.set(request, response, authData); + } else { + authStorage.clear(request, response); + } + } + } + + /** + * Returns any resource target to redirect to after successful + * authentication. This method either returns a non-empty string or + * <code>null</code>. First the <code>resource</code> request attribute is + * checked. If it is a non-empty string, it is returned. Second the + * <code>resource</code> request parameter is checked and returned if it is + * a non-empty string. + * + * @param request The request providing the attribute or parameter + * @return The non-empty redirection target or <code>null</code>. + */ + static String getLoginResource(final HttpServletRequest request) { + + // return the resource attribute if set to a non-empty string + Object resObj = request.getAttribute(Authenticator.LOGIN_RESOURCE); + if ((resObj instanceof String) && ((String) resObj).length() > 0) { + return (String) resObj; + } + + // return the resource parameter if not set or set to a non-empty value + final String resource = request.getParameter(Authenticator.LOGIN_RESOURCE); + if (resource == null || resource.length() > 0) { + return resource; + } + + // normalize empty resource string to null + return null; + } + // --------- Request Parameter Auth --------- private AuthenticationInfo extractRequestParameterAuthentication( @@ -419,7 +551,7 @@ public class FormAuthenticationHandler implements AuthenticationHandler, String user = request.getParameter(PAR_J_USERNAME); String pwd = request.getParameter(PAR_J_PASSWORD); - if (user != null && user.length() > 0 && pwd != null) { + if (user != null && pwd != null) { info = new AuthenticationInfo(HttpServletRequest.FORM_AUTH, user, pwd.toCharArray()); } diff --git a/src/main/java/org/apache/sling/formauth/FormReason.java b/src/main/java/org/apache/sling/formauth/FormReason.java new file mode 100644 index 0000000..256e9e8 --- /dev/null +++ b/src/main/java/org/apache/sling/formauth/FormReason.java @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.formauth; + +enum FormReason { + + /** + * The login form is request because the credentials previously entered very + * not valid to login to the repository. + */ + INVALID_CREDENTIALS { + @Override + public String getMessage() { + return "Username and Password do not match"; + } + }, + + /** + * The login form is requested because an existing session has timed out and + * the credentials have to be entered again. + */ + TIMEOUT { + @Override + public String getMessage() { + return "Session timed out, please login again"; + } + }; + + /** + * Returns an english indicative message of the reason to request the login + * form. + */ + abstract String getMessage(); +} diff --git a/src/main/resources/login.html b/src/main/resources/org/apache/sling/formauth/login.html similarity index 58% rename from src/main/resources/login.html rename to src/main/resources/org/apache/sling/formauth/login.html index 2d3c7a0..b9cb3d8 100644 --- a/src/main/resources/login.html +++ b/src/main/resources/org/apache/sling/formauth/login.html @@ -1,4 +1,24 @@ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> +<!-- + + Licensed to the Apache Software Foundation (ASF) under one + or more contributor license agreements. See the NOTICE file + distributed with this work for additional information + regarding copyright ownership. The ASF licenses this file + to you 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. + +--> <html xml:lang="en" lang="en" xmlns="http://www.w3.org/1999/xhtml" > @@ -26,6 +46,10 @@ padding: 0px; margin: 0px; } + + #err { + color: red; + } </style> </head> @@ -39,9 +63,13 @@ <input type="hidden" name="_charset_" value="UTF-8" /> <input type="hidden" name="resource" value="${resource}" /> - + + <div id="err"> + <p>${j_reason}</p> + </div> + <div> - <label for="j_username" accesskey="u">User ID:</label> + <label for="j_username" accesskey="u">Username:</label> </div> <div> <input id="j_username" name="j_username" type="text" /> diff --git a/src/test/java/org/apache/sling/formauth/FormReasonTest.java b/src/test/java/org/apache/sling/formauth/FormReasonTest.java new file mode 100644 index 0000000..d723ef4 --- /dev/null +++ b/src/test/java/org/apache/sling/formauth/FormReasonTest.java @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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.apache.sling.formauth; + +import junit.framework.TestCase; + +public class FormReasonTest extends TestCase { + + public void test_TIMEOUT() { + assertEquals(FormReason.TIMEOUT, + FormReason.valueOf(FormReason.TIMEOUT.toString())); + } + + public void test_INVALID_CREDENTIALS() { + assertEquals(FormReason.INVALID_CREDENTIALS, + FormReason.valueOf(FormReason.INVALID_CREDENTIALS.toString())); + } + + public void test_INVALID() { + try { + FormReason.valueOf("INVALID"); + fail("unexpected result getting value of an invalid constant"); + } catch (IllegalArgumentException iae) { + // expected + } + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
