You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 09:17:47 UTC

[sling-org-apache-sling-auth-form] 03/32: 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

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 <fm...@apache.org>
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
"commits@sling.apache.org" <co...@sling.apache.org>.