SLING-1650 Consolidate authentication stuff in a new auth component;
* rename commons/auth to auth/core
* rename extensions/formauth to auth/form
* rename extensions/openidauth to auth/openid

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@985662 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/README.txt b/README.txt
new file mode 100644
index 0000000..322c689
--- /dev/null
+++ b/README.txt
@@ -0,0 +1,30 @@
+Apache Sling Form Based Cookie Authenticator
+
+Bundle implementing form based authentication with login and logout support.
+Authentication state is maintained in a Cookie or in an HTTP Session. The
+password is only submitted when first authenticating.
+
+Getting Started
+===============
+
+This component uses a Maven 2 (http://maven.apache.org/) build
+environment. It requires a Java 5 JDK (or higher) and Maven
+(http://maven.apache.org/) 2.2.1 or later. We recommend to use the latest
+Maven version.
+
+If you have Maven 2 installed, you can compile and
+package the jar using the following command:
+
+    mvn package
+
+See the Maven 2 documentation for other build features.
+
+The latest source code for this component is available in the
+Subversion (http://subversion.tigris.org/) source repository of
+the Apache Software Foundation. If you have Subversion installed,
+you can checkout the latest source using the following command:
+
+    svn checkout http://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/formauth
+
+See the Subversion documentation for other source control features.
+
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..edd5d28
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,175 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+    <!--
+        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.
+    -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>9</version>
+        <relativePath>../../../parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.sling.auth.form</artifactId>
+    <version>0.9-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Form Based Authentication Handler</name>
+    <description>
+        Bundle implementing form based authentication with login
+        and logout support. Authentication state is maintained in
+        a Cookie or in an HTTP Session. The password is only submitted
+        when first authenticating.
+    </description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/bundles/auth/form</connection>
+        <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/auth/form</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/bundles/auth/form</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Bundle-DocURL>
+                            http://sling.apache.org/site/form-based-authenticationhandler.html
+                        </Bundle-DocURL>
+                        <Export-Package>
+                            org.apache.sling.auth.form;version=1.0
+                        </Export-Package>
+                        <Private-Package>
+                            org.apache.sling.auth.form.impl.*
+                        </Private-Package>
+                        <Import-Package>
+                            javax.security.auth.callback;
+                            javax.security.auth.login;
+                            org.apache.sling.jcr.jackrabbit.server.security;
+                            resolution:=optional,
+                            *
+                        </Import-Package>
+                        <Embed-Dependency>
+                            org.apache.sling.commons.osgi;inline="org/apache/sling/commons/osgi/OsgiUtil.*",
+                            commons-lang;inline="org/apache/commons/lang/StringUtils.class",
+                            commons-codec;inline="org/apache/commons/codec/binary/Base64.*
+                            |org/apache/commons/codec/binary/Hex*
+                            |org/apache/commons/codec/binary/StringUtils*
+                            |org/apache/commons/codec/BinaryEncoder*
+                            |org/apache/commons/codec/BinaryDecoder*
+                            |org/apache/commons/codec/Encoder*
+                            |org/apache/commons/codec/Decoder*
+                            |org/apache/commons/codec/EncoderException*
+                            |org/apache/commons/codec/DecoderException*"
+                        </Embed-Dependency>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+    <reporting>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-javadoc-plugin</artifactId>
+                <configuration>
+                    <excludePackageNames>
+                        org.apache.sling.auth.form
+                    </excludePackageNames>
+                </configuration>
+            </plugin>
+        </plugins>
+    </reporting>
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.auth</artifactId>
+            <version>0.9.0-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.jcr.jackrabbit.server</artifactId>
+            <version>2.0.4-incubator</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.0.9-SNAPSHOT</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+        </dependency>
+
+        <!-- Embedded Dependencies -->
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.commons.osgi</artifactId>
+            <version>2.0.2-incubator</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-codec</groupId>
+            <artifactId>commons-codec</artifactId>
+            <version>1.4</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-lang</groupId>
+            <artifactId>commons-lang</artifactId>
+            <version>2.4</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Test Dependencies -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.jmock</groupId>
+            <artifactId>jmock-junit4</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+        </dependency>
+    </dependencies>
+</project>
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..4510a21
--- /dev/null
+++ b/src/main/java/org/apache/sling/formauth/FormReason.java
@@ -0,0 +1,57 @@
+/*
+ * 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;
+
+public enum FormReason {
+
+    /**
+     * The login form is request because the credentials previously entered very
+     * not valid to login to the repository.
+     */
+    INVALID_CREDENTIALS("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("Session timed out, please login again");
+
+    /**
+     * The user-friendly message returned by {@link #toString()}
+     */
+    private final String message;
+
+    /**
+     * Creates an instance of the reason conveying the given descriptive reason.
+     *
+     * @param message The descriptive reason.
+     */
+    private FormReason(String message) {
+        this.message = message;
+    }
+
+    /**
+     * Returns the message set when constructing this instance. To get the
+     * official name call the <code>name()</code> method.
+     */
+    @Override
+    public String toString() {
+        return message;
+    }
+}
diff --git a/src/main/java/org/apache/sling/formauth/impl/AuthenticationFormServlet.java b/src/main/java/org/apache/sling/formauth/impl/AuthenticationFormServlet.java
new file mode 100644
index 0000000..1127f74
--- /dev/null
+++ b/src/main/java/org/apache/sling/formauth/impl/AuthenticationFormServlet.java
@@ -0,0 +1,84 @@
+/*
+ * 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.impl;
+
+import javax.servlet.http.HttpServletRequest;
+import org.apache.sling.commons.auth.spi.AbstractAuthenticationFormServlet;
+import org.apache.sling.formauth.FormReason;
+
+/**
+ * The <code>AuthenticationFormServlet</code> provides the default login form
+ * used for Form Based Authentication.
+ *
+ * @scr.component metatype="no"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ * @scr.property name="service.description"
+ *               value="Default Login Form for Form Based Authentication"
+ * @scr.service interface="javax.servlet.Servlet"
+ */
+@SuppressWarnings("serial")
+public class AuthenticationFormServlet extends AbstractAuthenticationFormServlet {
+
+    /**
+     * The constant is used 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;
+
+    /**
+     * 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
+     */
+    protected String getReason(final HttpServletRequest request) {
+        // return the resource attribute if set to a non-empty string
+        Object resObj = request.getAttribute(FormAuthenticationHandler.PAR_J_REASON);
+        if (resObj instanceof FormReason) {
+            return ((FormReason) resObj).toString();
+        }
+
+        final String reason = request.getParameter(FormAuthenticationHandler.PAR_J_REASON);
+        if (reason != null) {
+            try {
+                return FormReason.valueOf(reason).toString();
+            } catch (IllegalArgumentException iae) {
+                // thrown if the reason is not an expected value, assume none
+            }
+
+            // no valid FormReason value, use raw value
+            return reason;
+        }
+
+        return "";
+    }
+}
diff --git a/src/main/java/org/apache/sling/formauth/impl/FormAuthenticationHandler.java b/src/main/java/org/apache/sling/formauth/impl/FormAuthenticationHandler.java
new file mode 100644
index 0000000..1e9663b
--- /dev/null
+++ b/src/main/java/org/apache/sling/formauth/impl/FormAuthenticationHandler.java
@@ -0,0 +1,1003 @@
+/*
+ * 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.impl;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Dictionary;
+
+import javax.jcr.Credentials;
+import javax.jcr.SimpleCredentials;
+import javax.servlet.Servlet;
+import javax.servlet.ServletException;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpSession;
+
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.StringUtils;
+import org.apache.sling.api.auth.Authenticator;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.commons.auth.spi.AbstractAuthenticationHandler;
+import org.apache.sling.commons.auth.spi.AuthenticationInfo;
+import org.apache.sling.commons.auth.spi.DefaultAuthenticationFeedbackHandler;
+import org.apache.sling.commons.osgi.OsgiUtil;
+import org.apache.sling.formauth.FormReason;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>FormAuthenticationHandler</code> class implements the authorization
+ * steps based on a cookie.
+ *
+ * @scr.component immediate="false" label="%auth.form.name"
+ *                description="%auth.form.description"
+ *                name="org.apache.sling.formauth.FormAuthenticationHandler"
+ * @scr.property name="service.description"
+ *               value="Apache Sling Form Based Authentication Handler"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ * @scr.property nameRef=
+ *               "org.apache.sling.commons.auth.spi.AuthenticationHandler.PATH_PROPERTY"
+ *               values.0="/"
+ * @scr.property nameRef=
+ *               "org.apache.sling.commons.auth.spi.AuthenticationHandler.TYPE_PROPERTY"
+ *               valueRef="javax.servlet.http.HttpServletRequest.FORM_AUTH"
+ *               private="true"
+ * @scr.service
+ */
+public class FormAuthenticationHandler extends AbstractAuthenticationHandler {
+
+    /**
+     * The name of the parameter providing the login form URL.
+     *
+     * @scr.property valueRef="AuthenticationFormServlet.SERVLET_PATH"
+     */
+    private static final String PAR_LOGIN_FORM = "form.login.form";
+
+    /**
+     * @scr.property valueRef="DEFAULT_AUTH_STORAGE" options "cookie"="Cookie"
+     *               "session"="Session Attribute"
+     */
+    private static final String PAR_AUTH_STORAGE = "form.auth.storage";
+
+    /**
+     * The value of the {@link #PAR_AUTH_STORAGE} parameter indicating the use
+     * of a Cookie to store the authentication data.
+     */
+    private static final String AUTH_STORAGE_COOKIE = "cookie";
+
+    /**
+     * The value of the {@link #PAR_AUTH_STORAGE} parameter indicating the use
+     * of a session attribute to store the authentication data.
+     */
+    private static final String AUTH_STORAGE_SESSION_ATTRIBUTE = "session";
+
+    /**
+     * To be used to determine if the auth has value comes from a cookie or from
+     * a session attribute.
+     */
+    private static final String DEFAULT_AUTH_STORAGE = AUTH_STORAGE_COOKIE;
+
+    /**
+     * The name of the configuration parameter providing the Cookie or session
+     * attribute name.
+     *
+     * @scr.property valueRef="DEFAULT_AUTH_NAME"
+     */
+    private static final String PAR_AUTH_NAME = "form.auth.name";
+
+    /**
+     * The default Cookie or session attribute name
+     *
+     * @see #PAR_AUTH_NAME
+     */
+    private static final String DEFAULT_AUTH_NAME = "sling.formauth";
+
+    /**
+     * This is the name of the SimpleCredentials attribute that holds the auth
+     * info extracted from the cookie value.
+     *
+     * @scr.property valueRef="DEFAULT_CREDENTIALS_ATTRIBUTE_NAME"
+     */
+    private static final String PAR_CREDENTIALS_ATTRIBUTE_NAME = "form.credentials.name";
+
+    /**
+     * Default value for the {@link #PAR_CREDENTIALS_ATTRIBUTE_NAME} property
+     */
+    private static final String DEFAULT_CREDENTIALS_ATTRIBUTE_NAME = DEFAULT_AUTH_NAME;
+
+    /**
+     * The number of minutes after which a login session times out. This value
+     * is used as the expiry time set in the authentication data.
+     *
+     * @scr.property type="Integer" valueRef="DEFAULT_AUTH_TIMEOUT"
+     */
+    public static final String PAR_AUTH_TIMEOUT = "form.auth.timeout";
+
+    /**
+     * The default authentication data time out value.
+     *
+     * @see #PAR_AUTH_TIMEOUT
+     */
+    private static final int DEFAULT_AUTH_TIMEOUT = 30;
+
+    /**
+     * The name of the file used to persist the security tokens
+     *
+     * @scr.property valueRef="DEFAULT_TOKEN_FILE"
+     */
+    private static final String PAR_TOKEN_FILE = "form.token.file";
+
+    private static final String DEFAULT_TOKEN_FILE = "cookie-tokens.bin";
+
+    /**
+     * Whether to redirect to the login form or simple do an include.
+     *
+     * @scr.property type="Boolean" valueRef="DEFAULT_INCLUDE_FORM"
+     */
+    public static final String PAR_INCLUDE_FORM = "form.use.include";
+
+    /**
+     * The default include value.
+     *
+     * @see #PAR_INCLUDE_FORM
+     */
+    private static final boolean DEFAULT_INCLUDE_FORM = false;
+
+
+    /**
+     * Whether to present a login form when a users cookie expires, the default
+     * is not to present the form.
+     *
+     * @scr.property type="Boolean" valueRef="DEFAULT_LOGIN_AFTER_EXPIRE"
+     */
+    private static final String PAR_LOGIN_AFTER_EXPIRE = "form.onexpire.login";
+
+    /**
+     * The default login after expire of a cookie.
+     *
+     * @see #PAR_LOGIN_AFTER_EXPIRE
+     */
+    private static final boolean DEFAULT_LOGIN_AFTER_EXPIRE = false;
+
+    /**
+     * The request method required for user name and password submission by the
+     * form (value is "POST").
+     */
+    private static final String REQUEST_METHOD = "POST";
+
+    /**
+     * The last segment of the request URL for the user name and password
+     * submission by the form (value is "/j_security_check").
+     * <p>
+     * This name is derived from the prescription in the Servlet API 2.4
+     * Specification, Section SRV.12.5.3.1 Login Form Notes: <i>In order for the
+     * authentication to proceeed appropriately, the action of the login form
+     * must always be set to <code>j_security_check</code>.</i>
+     */
+    private static final String REQUEST_URL_SUFFIX = "/j_security_check";
+
+    /**
+     * The name of the form submission parameter providing the name of the user
+     * to authenticate (value is "j_username").
+     * <p>
+     * This name is prescribed by the Servlet API 2.4 Specification, Section
+     * SRV.12.5.3 Form Based Authentication.
+     */
+    private static final String PAR_J_USERNAME = "j_username";
+
+    /**
+     * The name of the form submission parameter providing the password of the
+     * user to authenticate (value is "j_password").
+     * <p>
+     * This name is prescribed by the Servlet API 2.4 Specification, Section
+     * SRV.12.5.3 Form Based Authentication.
+     */
+    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 service ranking property.
+     *
+     * @scr.property type="Integer" value="0" private="false"
+     */
+    @SuppressWarnings("unused")
+    private static final String PAR_SERVICE_RANKING = Constants.SERVICE_RANKING;
+
+    /**
+     * 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());
+
+    private AuthenticationStorage authStorage;
+
+    private String loginForm;
+
+    /**
+     * The timeout of a login session in milliseconds, converted from the
+     * configuration property {@link #PAR_AUTH_TIMEOUT} by multiplying with
+     * {@link #MINUTES}.
+     */
+    private long sessionTimeout;
+
+    /**
+     * The name of the credentials attribute which is set to the cookie data
+     * to validate.
+     */
+    private String attrCookieAuthData;
+
+    /**
+     * The {@link TokenStore} used to persist and check authentication data
+     */
+    private TokenStore tokenStore;
+
+    /**
+     * The {@link FormLoginModulePlugin} service registration created when
+     * this authentication handler is registered. If the login module plugin
+     * cannot be created this field is set to <code>null</code>.
+     */
+    private ServiceRegistration loginModule;
+
+    /**
+     * If true, the handler will attempt to include the login form instead of
+     * doing a redirect.
+     */
+    private boolean includeLoginForm;
+
+    /**
+     * The resource resolver factory used to resolve the login form as a resource
+     *
+     * @scr.reference policy="dynamic" cardinality="0..1"
+     */
+    private ResourceResolverFactory resourceResolverFactory;
+
+    /**
+     * If true the login form will be presented when the token expires.
+     */
+    private boolean loginAfterExpire;
+
+
+    /**
+     * Extracts cookie/session based credentials from the request. Returns
+     * <code>null</code> if the handler assumes HTTP Basic authentication would
+     * be more appropriate, if no form fields are present in the request and if
+     * the secure user data is not present either in the cookie or an HTTP
+     * Session.
+     */
+    public AuthenticationInfo extractCredentials(HttpServletRequest request,
+            HttpServletResponse response) {
+
+        AuthenticationInfo info = null;
+
+        // 1. try credentials from POST'ed request parameters
+        info = this.extractRequestParameterAuthentication(request);
+
+        // 2. try credentials from the cookie or session
+        if (info == null) {
+            String authData = authStorage.extractAuthenticationInfo(request);
+            if (authData != null) {
+                if (tokenStore.isValid(authData)) {
+                    info = createAuthInfo(authData);
+                } else {
+                    if (this.loginAfterExpire) {
+                      // signal the requestCredentials method a previous login failure
+                      request.setAttribute(PAR_J_REASON, FormReason.TIMEOUT);
+                      info = AuthenticationInfo.FAIL_AUTH;
+                    }
+                    // clear the cookie, its invalid and we should get rid of it so that the invalid cookie
+                    // isn't present on the authN operation.
+                    authStorage.clear(request, response);
+                }
+            }
+        }
+
+        return info;
+    }
+
+    /**
+     * 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;
+        }
+
+        // 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;
+        }
+
+        String resource = getLoginResource(request, null);
+        if (resource == null) {
+            resource = request.getContextPath() + request.getPathInfo();
+            request.setAttribute(Authenticator.LOGIN_RESOURCE, resource);
+        }
+
+        if (includeLoginForm && (resourceResolverFactory != null)) {
+            ResourceResolver resourceResolver = null;
+            try {
+                resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null);
+                Resource loginFormResource = resourceResolver.resolve(loginForm);
+                Servlet loginFormServlet = loginFormResource.adaptTo(Servlet.class);
+                if (loginFormServlet != null) {
+                    try {
+                        loginFormServlet.service(request, response);
+                        return true;
+                    } catch (ServletException e) {
+                        log.error("Failed to include the form: " + loginForm, e);
+                    }
+                }
+            } catch (LoginException e) {
+                log.error("Unable to get a resource resolver to include for the login resource. Will redirect instead.");
+            } finally {
+                if (resourceResolver != null) {
+                    resourceResolver.close();
+                }
+            }
+        }
+
+        // prepare the login form redirection target
+        final StringBuilder targetBuilder = new StringBuilder();
+        targetBuilder.append(request.getContextPath());
+        targetBuilder.append(loginForm);
+
+        // append originally requested resource (for redirect after login)
+        char parSep = '?';
+
+        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 Object jReason = request.getAttribute(PAR_J_REASON);
+            @SuppressWarnings("unchecked")
+            final String reason = (jReason instanceof Enum)
+                    ? ((Enum) jReason).name()
+                    : jReason.toString();
+            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);
+        } catch (IOException e) {
+            log.error("Failed to redirect to the page: " + target, e);
+        }
+
+        return true;
+    }
+
+    /**
+     * Clears all authentication state which might have been prepared by this
+     * authentication handler.
+     */
+    public void dropCredentials(HttpServletRequest request,
+            HttpServletResponse response) {
+        authStorage.clear(request, response);
+    }
+
+    // ---------- AuthenticationFeedbackHandler
+
+    /**
+     * Called after an unsuccessful login attempt. This implementation makes
+     * sure the authentication data is removed either by removing the cookie or
+     * by remove the HTTP Session attribute.
+     */
+    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);
+    }
+
+    /**
+     * Called after successfull login with the given authentication info. This
+     * implementation ensures the authentication data is set in either the
+     * cookie or the HTTP session with the correct security tokens.
+     * <p>
+     * If no authentication data already exists, it is created. Otherwise if the
+     * data has expired the data is updated with a new security token and a new
+     * expiry time.
+     * <p>
+     * If creating or updating the authentication data fails, it is actually
+     * removed from the cookie or the HTTP session and future requests will not
+     * be authenticated any longer.
+     */
+    public boolean authenticationSucceeded(HttpServletRequest request,
+            HttpServletResponse response, AuthenticationInfo authInfo) {
+
+        /*
+         * Note: This method is called if this handler provided credentials
+         * which succeeded loging into the repository
+         */
+
+        // ensure fresh authentication data
+        refreshAuthData(request, response, authInfo);
+
+        final boolean result;
+        if (isValidateRequest(request)) {
+
+            try {
+                response.setStatus(200);
+                response.flushBuffer();
+            } catch (IOException ioe) {
+                log.error("Failed to send 200/OK response", ioe);
+            }
+
+            // terminate request, all done
+            result = true;
+
+        } else if (DefaultAuthenticationFeedbackHandler.handleRedirect(
+            request, response)) {
+
+            // 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, null);
+            if (resource != null) {
+                try {
+                    response.sendRedirect(resource);
+                } catch (IOException ioe) {
+                    log.error("Failed to send redirect to: " + resource, ioe);
+                }
+
+                // terminate request, all done
+                result = true;
+            } else {
+                // no redirect, hence continue processing
+                result = false;
+            }
+
+        }
+
+        // no redirect
+        return result;
+    }
+
+    @Override
+    public String toString() {
+        return "Form Based Authentication Handler";
+    }
+
+    // --------- Force HTTP Basic Auth ---------
+
+    /**
+     * Returns <code>true</code> if this authentication handler should ignore
+     * the call to
+     * {@link #requestCredentials(HttpServletRequest, HttpServletResponse)}.
+     * <p>
+     * This method returns <code>true</code> if the
+     * {@link #REQUEST_LOGIN_PARAMETER} is set to any value other than "Form"
+     * (HttpServletRequest.FORM_AUTH).
+     */
+    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);
+
+        // 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);
+            }
+        }
+    }
+
+    // --------- Request Parameter Auth ---------
+
+    private AuthenticationInfo extractRequestParameterAuthentication(
+            HttpServletRequest request) {
+        AuthenticationInfo info = null;
+
+        // only consider login form parameters if this is a POST request
+        // to the j_security_check URL
+        if (REQUEST_METHOD.equals(request.getMethod())
+            && request.getRequestURI().endsWith(REQUEST_URL_SUFFIX)) {
+
+            String user = request.getParameter(PAR_J_USERNAME);
+            String pwd = request.getParameter(PAR_J_PASSWORD);
+
+            if (user != null && pwd != null) {
+                info = new AuthenticationInfo(HttpServletRequest.FORM_AUTH,
+                    user, pwd.toCharArray());
+
+                // if this request is providing form credentials, we have to
+                // make sure, that the request is redirected after successful
+                // authentication, otherwise the request may be processed
+                // as a POST request to the j_security_check page (unless
+                // the j_validate parameter is set)
+                if (getLoginResource(request, null) == null) {
+                    request.setAttribute(Authenticator.LOGIN_RESOURCE, "/");
+                }
+            }
+        }
+
+        return info;
+    }
+
+    private AuthenticationInfo createAuthInfo(final String authData) {
+        final String userId = getUserId(authData);
+        if (userId == null) {
+            return null;
+        }
+
+        final AuthenticationInfo info = new AuthenticationInfo(
+            HttpServletRequest.FORM_AUTH, userId);
+        info.put(attrCookieAuthData, authData);
+
+        return info;
+    }
+
+    private String getCookieAuthData(final AuthenticationInfo info) {
+        Object data = info.get(attrCookieAuthData);
+        if (data instanceof String) {
+            return (String) data;
+        }
+        return null;
+    }
+
+    // ---------- LoginModulePlugin support
+
+    private String getCookieAuthData(final Credentials credentials) {
+        if (credentials instanceof SimpleCredentials) {
+            Object data = ((SimpleCredentials) credentials).getAttribute(attrCookieAuthData);
+            if (data instanceof String) {
+                return (String) data;
+            }
+        }
+
+        // no SimpleCredentials or no valid attribute
+        return null;
+    }
+
+    boolean hasAuthData(final Credentials credentials) {
+        return getCookieAuthData(credentials) != null;
+    }
+
+    boolean isValid(final Credentials credentials) {
+        String authData = getCookieAuthData(credentials);
+        if (authData != null) {
+            return tokenStore.isValid(authData);
+        }
+
+        // no authdata, not valid
+        return false;
+    }
+
+    // ---------- SCR Integration ----------------------------------------------
+
+    /**
+     * Called by SCR to activate the authentication handler.
+     *
+     * @throws InvalidKeyException
+     * @throws NoSuchAlgorithmException
+     * @throws IllegalStateException
+     * @throws UnsupportedEncodingException
+     */
+    protected void activate(ComponentContext componentContext)
+            throws InvalidKeyException, NoSuchAlgorithmException,
+            IllegalStateException, UnsupportedEncodingException {
+
+        Dictionary<?, ?> properties = componentContext.getProperties();
+
+        this.loginForm = OsgiUtil.toString(properties.get(PAR_LOGIN_FORM),
+            AuthenticationFormServlet.SERVLET_PATH);
+        log.info("Login Form URL {}", loginForm);
+
+        final String authName = OsgiUtil.toString(
+            properties.get(PAR_AUTH_NAME), DEFAULT_AUTH_NAME);
+        final String authStorage = OsgiUtil.toString(
+            properties.get(PAR_AUTH_STORAGE), DEFAULT_AUTH_STORAGE);
+        if (AUTH_STORAGE_SESSION_ATTRIBUTE.equals(authStorage)) {
+
+            this.authStorage = new SessionStorage(authName);
+            log.info("Using HTTP Session store with attribute name {}",
+                authName);
+
+        } else {
+
+            this.authStorage = new CookieStorage(authName);
+            log.info("Using Cookie store with name {}", authName);
+
+        }
+
+        this.attrCookieAuthData = OsgiUtil.toString(
+            properties.get(PAR_CREDENTIALS_ATTRIBUTE_NAME),
+            DEFAULT_CREDENTIALS_ATTRIBUTE_NAME);
+        log.info("Setting Auth Data attribute name {}", attrCookieAuthData);
+
+        int timeoutMinutes = OsgiUtil.toInteger(
+            properties.get(PAR_AUTH_TIMEOUT), DEFAULT_AUTH_TIMEOUT);
+        if (timeoutMinutes < 1) {
+            timeoutMinutes = DEFAULT_AUTH_TIMEOUT;
+        }
+        log.info("Setting session timeout {} minutes", timeoutMinutes);
+        this.sessionTimeout = MINUTES * timeoutMinutes;
+
+        final String tokenFileName = OsgiUtil.toString(
+            properties.get(PAR_TOKEN_FILE), DEFAULT_TOKEN_FILE);
+        final File tokenFile = getTokenFile(tokenFileName,
+            componentContext.getBundleContext());
+        log.info("Storing tokens in {}", tokenFile.getAbsolutePath());
+        this.tokenStore = new TokenStore(tokenFile, sessionTimeout);
+
+        this.loginModule = null;
+        try {
+            this.loginModule = FormLoginModulePlugin.register(this,
+                componentContext.getBundleContext());
+        } catch (Throwable t) {
+            log.info("Cannot register FormLoginModulePlugin. This is expected if Sling LoginModulePlugin services are not supported");
+            log.debug("dump", t);
+        }
+
+        this.includeLoginForm = OsgiUtil.toBoolean(properties.get(PAR_INCLUDE_FORM), DEFAULT_INCLUDE_FORM);
+
+        this.loginAfterExpire = OsgiUtil.toBoolean(properties.get(PAR_LOGIN_AFTER_EXPIRE), DEFAULT_LOGIN_AFTER_EXPIRE);
+
+    }
+
+    protected void deactivate(
+            @SuppressWarnings("unused") ComponentContext componentContext) {
+        if (loginModule != null) {
+            loginModule.unregister();
+            loginModule = null;
+        }
+    }
+
+    /**
+     * Returns an absolute file indicating the file to use to persist the
+     * security tokens.
+     * <p>
+     * This method is not part of the API of this class and is package private
+     * to enable unit tests.
+     *
+     * @param tokenFileName The configured file name, must not be null
+     * @param bundleContext The BundleContext to use to make an relative file
+     *            absolute
+     * @return The absolute file
+     */
+    File getTokenFile(final String tokenFileName,
+            final BundleContext bundleContext) {
+        File tokenFile = new File(tokenFileName);
+        if (tokenFile.isAbsolute()) {
+            return tokenFile;
+        }
+
+        tokenFile = bundleContext.getDataFile(tokenFileName);
+        if (tokenFile == null) {
+            final String slingHome = bundleContext.getProperty("sling.home");
+            if (slingHome != null) {
+                tokenFile = new File(slingHome, tokenFileName);
+            } else {
+                tokenFile = new File(tokenFileName);
+            }
+        }
+
+        return tokenFile.getAbsoluteFile();
+    }
+
+    /**
+     * Returns the user id from the authentication data. If the authentication
+     * data is a non-<code>null</code> value with 3 fields separated by an @
+     * sign, the value of the third field is returned. Otherwise
+     * <code>null</code> is returned.
+     * <p>
+     * This method is not part of the API of this class and is package private
+     * to enable unit tests.
+     *
+     * @param authData
+     * @return
+     */
+    String getUserId(final String authData) {
+        if (authData != null) {
+            String[] parts = StringUtils.split(authData, "@");
+            if (parts != null && parts.length == 3) {
+                return parts[2];
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Refresh the cookie periodically.
+     *
+     * @param sessionTimeout time to live for the session
+     * @return true or false
+     */
+    private boolean needsRefresh(final String authData,
+            final long sessionTimeout) {
+        boolean updateCookie = false;
+        if (authData == null) {
+            updateCookie = true;
+        } else {
+            String[] parts = StringUtils.split(authData, "@");
+            if (parts != null && parts.length == 3) {
+                long cookieTime = Long.parseLong(parts[1].substring(1));
+                if (System.currentTimeMillis() + (sessionTimeout / 2) > cookieTime) {
+                    updateCookie = true;
+                }
+            }
+        }
+        return updateCookie;
+    }
+
+    /**
+     * The <code>AuthenticationStorage</code> interface abstracts the API
+     * required to store the {@link CookieAuthData} in an HTTP cookie or in an
+     * HTTP Session. The concrete class -- {@link CookieExtractor} or
+     * {@link SessionExtractor} -- is selected using the
+     * {@link CookieAuthenticationHandler#PAR_AUTH_HASH_STORAGE} configuration
+     * parameter, {@link CookieExtractor} by default.
+     */
+    private static interface AuthenticationStorage {
+        String extractAuthenticationInfo(HttpServletRequest request);
+
+        void set(HttpServletRequest request, HttpServletResponse response,
+                String authData);
+
+        void clear(HttpServletRequest request, HttpServletResponse response);
+    }
+
+    /**
+     * The <code>CookieExtractor</code> class supports storing the
+     * {@link CookieAuthData} in an HTTP Cookie.
+     */
+    private static class CookieStorage implements AuthenticationStorage {
+        private final String cookieName;
+
+        public CookieStorage(final String cookieName) {
+            this.cookieName = cookieName;
+        }
+
+        public String extractAuthenticationInfo(HttpServletRequest request) {
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if (this.cookieName.equals(cookie.getName())) {
+                        // found the cookie, so try to extract the credentials
+                        // from it
+                        String value = cookie.getValue();
+
+                        // reverse the base64 encoding
+                        try {
+                            return new String(Base64.decodeBase64(value),
+                                "UTF-8");
+                        } catch (UnsupportedEncodingException e1) {
+                            throw new RuntimeException(e1);
+                        }
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        public void set(HttpServletRequest request,
+                HttpServletResponse response, String authData) {
+            // base64 encode to handle any special characters
+            String cookieValue;
+            try {
+                cookieValue = Base64.encodeBase64URLSafeString(authData.getBytes("UTF-8"));
+            } catch (UnsupportedEncodingException e1) {
+                throw new RuntimeException(e1);
+            }
+
+            // send the cookie to the response
+            setCookie(request, response, cookieValue, -1);
+        }
+
+        public void clear(HttpServletRequest request,
+                HttpServletResponse response) {
+            Cookie oldCookie = null;
+            Cookie[] cookies = request.getCookies();
+            if (cookies != null) {
+                for (Cookie cookie : cookies) {
+                    if (this.cookieName.equals(cookie.getName())) {
+                        // found the cookie
+                        oldCookie = cookie;
+                        break;
+                    }
+                }
+            }
+
+            // remove the old cookie from the client
+            if (oldCookie != null) {
+                setCookie(request, response, "", 0);
+            }
+        }
+
+        private void setCookie(final HttpServletRequest request,
+                final HttpServletResponse response, final String value,
+                final int age) {
+
+            final String ctxPath = request.getContextPath();
+            final String cookiePath = (ctxPath == null || ctxPath.length() == 0)
+                    ? "/"
+                    : ctxPath;
+
+            Cookie cookie = new Cookie(this.cookieName, value);
+            cookie.setMaxAge(age);
+            cookie.setPath(cookiePath);
+            cookie.setSecure(request.isSecure());
+            response.addCookie(cookie);
+        }
+    }
+
+    /**
+     * The <code>SessionExtractor</code> class provides support to store the
+     * {@link CookieAuthData} in an HTTP Session.
+     */
+    private static class SessionStorage implements AuthenticationStorage {
+        private final String sessionAttributeName;
+
+        SessionStorage(final String sessionAttributeName) {
+            this.sessionAttributeName = sessionAttributeName;
+        }
+
+        public String extractAuthenticationInfo(HttpServletRequest request) {
+            HttpSession session = request.getSession(false);
+            if (session != null) {
+                Object attribute = session.getAttribute(sessionAttributeName);
+                if (attribute instanceof String) {
+                    return (String) attribute;
+                }
+            }
+            return null;
+        }
+
+        public void set(HttpServletRequest request,
+                HttpServletResponse response, String authData) {
+            // store the auth hash as a session attribute
+            HttpSession session = request.getSession();
+            session.setAttribute(sessionAttributeName, authData);
+        }
+
+        public void clear(HttpServletRequest request,
+                HttpServletResponse response) {
+            HttpSession session = request.getSession(false);
+            if (session != null) {
+                session.removeAttribute(sessionAttributeName);
+            }
+        }
+
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/formauth/impl/FormLoginModulePlugin.java b/src/main/java/org/apache/sling/formauth/impl/FormLoginModulePlugin.java
new file mode 100644
index 0000000..79d9fa1
--- /dev/null
+++ b/src/main/java/org/apache/sling/formauth/impl/FormLoginModulePlugin.java
@@ -0,0 +1,153 @@
+/*
+ * 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.impl;
+
+import java.security.Principal;
+import java.util.Hashtable;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Credentials;
+import javax.jcr.Session;
+import javax.security.auth.callback.CallbackHandler;
+import org.apache.sling.jcr.jackrabbit.server.security.AuthenticationPlugin;
+import org.apache.sling.jcr.jackrabbit.server.security.LoginModulePlugin;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceRegistration;
+
+/**
+ * The <code>FormLoginModulePlugin</code> is a LoginModulePlugin which handles
+ * <code>SimpleCredentials</code> attributed with the special authentication
+ * data provided by the {@link FormAuthenticationHandler}.
+ * <p>
+ * This class is instantiated by the {@link FormAuthenticationHandler} calling
+ * the {@link #register(FormAuthenticationHandler, BundleContext)} method. If
+ * the OSGi framework does not provide the <code>LoginModulePlugin</code>
+ * interface (such as when the Sling Jackrabbit Server bundle is not used to
+ * provide the JCR Repository), loading this class fails, which is caught by the
+ * {@link FormAuthenticationHandler}.
+ */
+final class FormLoginModulePlugin implements LoginModulePlugin {
+
+    /**
+     * The {@link FormAuthenticationHandler} used to validate the credentials
+     * and its contents.
+     */
+    private final FormAuthenticationHandler authHandler;
+
+    /**
+     * Creates an instance of this class and registers it as a
+     * <code>LoginModulePlugin</code> service to handle login requests with
+     * <code>SimpleCredentials</code> provided by the
+     * {@link FormAuthenticationHandler}.
+     *
+     * @param authHandler The {@link FormAuthenticationHandler} providing
+     *            support to validate the credentials
+     * @param bundleContext The <code>BundleContext</code> to register the
+     *            service
+     * @return The <code>ServiceRegistration</code> of the registered service for
+     *         the {@link FormAuthenticationHandler} to unregister the service
+     *         on shutdown.
+     */
+    static ServiceRegistration register(
+            final FormAuthenticationHandler authHandler,
+            final BundleContext bundleContext) {
+        FormLoginModulePlugin plugin = new FormLoginModulePlugin(authHandler);
+
+        Hashtable<String, Object> properties = new Hashtable<String, Object>();
+        properties.put(Constants.SERVICE_DESCRIPTION,
+            "LoginModulePlugin Support for FormAuthenticationHandler");
+        properties.put(Constants.SERVICE_VENDOR,
+            bundleContext.getBundle().getHeaders().get(Constants.BUNDLE_VENDOR));
+
+        return bundleContext.registerService(LoginModulePlugin.class.getName(),
+            plugin, properties);
+    }
+
+    /**
+     * Private constructor called from
+     * {@link #register(FormAuthenticationHandler, BundleContext)} to create an
+     * instance of this class.
+     *
+     * @param authHandler The {@link FormAuthenticationHandler} used to validate
+     *            the credentials attribute
+     */
+    private FormLoginModulePlugin(final FormAuthenticationHandler authHandler) {
+        this.authHandler = authHandler;
+    }
+
+    /**
+     * Returns <code>true</code> indicating support if the credentials is a
+     * <code>SimplerCredentials</code> object and has an authentication data
+     * attribute.
+     *
+     * @see CookieAuthenticationHandler#hasAuthData(Credentials)
+     */
+    public boolean canHandle(Credentials credentials) {
+        return authHandler.hasAuthData(credentials);
+    }
+
+    /**
+     * This implementation does nothing.
+     */
+    @SuppressWarnings("unchecked")
+    public void doInit(CallbackHandler callbackHandler, Session session,
+            Map options) {
+    }
+
+    /**
+     * Returns <code>null</code> to have the <code>DefaultLoginModule</code>
+     * provide a principal based on an existing user defined in the repository.
+     */
+    public Principal getPrincipal(final Credentials credentials) {
+        return null;
+    }
+
+    /**
+     * This implementation does nothing.
+     */
+    @SuppressWarnings("unchecked")
+    public void addPrincipals(@SuppressWarnings("unused") Set principals) {
+    }
+
+    /**
+     * Returns an <code>AuthenticationPlugin</code> which authenticates the
+     * credentials if the contain authentication data and the authentication
+     * data can is valid.
+     *
+     * @see CookieAuthenticationHandler#isValid(Credentials)
+     */
+    public AuthenticationPlugin getAuthentication(Principal principal,
+            Credentials creds) {
+        return new AuthenticationPlugin() {
+            public boolean authenticate(Credentials credentials) {
+                return authHandler.isValid(credentials);
+            }
+        };
+    }
+
+    /**
+     * Returns <code>LoginModulePlugin.IMPERSONATION_DEFAULT</code> to indicate
+     * that this plugin does not itself handle impersonation requests.
+     */
+    public int impersonate(Principal principal, Credentials credentials) {
+        return LoginModulePlugin.IMPERSONATION_DEFAULT;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/formauth/impl/TokenStore.java b/src/main/java/org/apache/sling/formauth/impl/TokenStore.java
new file mode 100644
index 0000000..2fe06ea
--- /dev/null
+++ b/src/main/java/org/apache/sling/formauth/impl/TokenStore.java
@@ -0,0 +1,380 @@
+/*
+ * Licensed to the Sakai Foundation (SF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The SF 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.impl;
+
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import javax.crypto.Mac;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>TokenStore</code> class provides the secure token hash
+ * implementation used by the {@link FormAuthenticationHandler} to generate,
+ * validate and persist secure tokens.
+ */
+class TokenStore {
+
+    /**
+     * Array of hex characters used by {@link #byteToHex(byte[])} to convert a
+     * byte array to a hex string.
+     */
+    private static final char[] TOHEX = "0123456789abcdef".toCharArray();
+
+    /**
+     * Name of the <code>SecureRandom</code> generator algorithm
+     */
+    private static final String SHA1PRNG = "SHA1PRNG";
+
+    /**
+     * The name of the HMAC function to calculate the hash code of the payload
+     * with the secure token.
+     */
+    private static final String HMAC_SHA1 = "HmacSHA1";
+
+    /**
+     * String encoding to convert byte arrays to strings and vice-versa.
+     */
+    private static final String UTF_8 = "UTF-8";
+
+    /** The number of secret keys in the token buffer currentTokens */
+    private static final int TOKEN_BUFFER_SIZE = 5;
+
+    public final Logger log = LoggerFactory.getLogger(TokenStore.class);
+
+    /**
+     * The ttl of the cookie before it becomes invalid (in ms)
+     */
+    private final long ttl;
+
+    /**
+     * The time when a new token should be created.
+     */
+    private long nextUpdate = System.currentTimeMillis();
+
+    /**
+     * The location of the current token.
+     */
+    private volatile int currentToken = 0;
+
+    /**
+     * A ring of tokens used to encypt.
+     */
+    private volatile SecretKey[] currentTokens;
+
+    /**
+     * A secure random used for generating new tokens.
+     */
+    private SecureRandom random;
+
+    /** The token file to persist the secure tokens */
+    private File tokenFile;
+
+    /** A temporary file used to update the secure token file */
+    private File tmpTokenFile;
+
+    /**
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     * @throws UnsupportedEncodingException
+     * @throws IllegalStateException
+     * @throws NullPointerException if <code>tokenFile</code> is
+     *             <code>null</code>.
+     */
+    TokenStore(final File tokenFile, final long sessionTimeout)
+            throws NoSuchAlgorithmException, InvalidKeyException,
+            IllegalStateException, UnsupportedEncodingException {
+
+        if (tokenFile == null) {
+            throw new NullPointerException("tokenfile");
+        }
+
+        this.random = SecureRandom.getInstance(SHA1PRNG);
+        this.ttl = sessionTimeout;
+        this.tokenFile = tokenFile;
+        this.tmpTokenFile = new File(tokenFile + ".tmp");
+
+        // prime the secret keys from persistence
+        loadTokens();
+
+        // warm up the crypto API
+        byte[] b = new byte[20];
+        random.nextBytes(b);
+        final SecretKey secretKey = new SecretKeySpec(b, HMAC_SHA1);
+        final Mac m = Mac.getInstance(HMAC_SHA1);
+        m.init(secretKey);
+        m.update(UTF_8.getBytes(UTF_8));
+        m.doFinal();
+    }
+
+    /**
+     * @param expires
+     * @param userId
+     * @return
+     * @throws UnsupportedEncodingException
+     * @throws IllegalStateException
+     * @throws NoSuchAlgorithmException
+     * @throws InvalidKeyException
+     */
+    String encode(final long expires, final String userId)
+            throws IllegalStateException, UnsupportedEncodingException,
+            NoSuchAlgorithmException, InvalidKeyException {
+        int token = getActiveToken();
+        SecretKey key = currentTokens[token];
+        return encode(expires, userId, token, key);
+    }
+
+    private String encode(final long expires, final String userId,
+            final int token, final SecretKey key) throws IllegalStateException,
+            UnsupportedEncodingException, NoSuchAlgorithmException,
+            InvalidKeyException {
+
+        String cookiePayload = String.valueOf(token) + String.valueOf(expires)
+            + "@" + userId;
+        Mac m = Mac.getInstance(HMAC_SHA1);
+        m.init(key);
+        m.update(cookiePayload.getBytes(UTF_8));
+        String cookieValue = byteToHex(m.doFinal());
+        return cookieValue + "@" + cookiePayload;
+    }
+
+    /**
+     * Returns <code>true</code> if the <code>value</code> is a valid secure
+     * token as follows:
+     * <ul>
+     * <li>The string is not <code>null</code></li>
+     * <li>The string contains three fields separated by an @ sign</li>
+     * <li>The expiry time encoded in the second field has not yet passed</li>
+     * <li>The hashing the third field, the expiry time and token number with
+     * the secure token (indicated by the token number) gives the same value as
+     * contained in the first field</li>
+     * </ul>
+     * <p>
+     * Otherwise the method returns <code>false</code>.
+     */
+    boolean isValid(String value) {
+        String[] parts = StringUtils.split(value, "@");
+        if (parts != null && parts.length == 3) {
+
+            // single digit token number
+            int tokenNumber = parts[1].charAt(0) - '0';
+            if (tokenNumber >= 0 && tokenNumber < currentTokens.length) {
+
+                long cookieTime = Long.parseLong(parts[1].substring(1));
+                if (System.currentTimeMillis() < cookieTime) {
+
+                    try {
+                        SecretKey secretKey = currentTokens[tokenNumber];
+                        String hmac = encode(cookieTime, parts[2], tokenNumber,
+                            secretKey);
+                        return value.equals(hmac);
+                    } catch (ArrayIndexOutOfBoundsException e) {
+                        log.error(e.getMessage(), e);
+                    } 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);
+                    }
+
+                    log.error("AuthNCookie value '{}' is invalid", value);
+
+                } else {
+                    log.error("AuthNCookie value '{}' has expired {}ms ago",
+                        value, (System.currentTimeMillis() - cookieTime));
+                }
+
+            } else {
+                log.error(
+                    "AuthNCookie value '{}' is invalid: refers to an invalid token number",
+                    value, tokenNumber);
+            }
+
+        } else {
+            log.error("AuthNCookie value '{}' has invalid format", value);
+        }
+
+        // failed verification, reason is logged
+        return false;
+    }
+
+    /**
+     * Maintain a circular buffer to tokens, and return the current one.
+     *
+     * @return the current token.
+     */
+    private synchronized int getActiveToken() {
+        if (System.currentTimeMillis() > nextUpdate
+            || currentTokens[currentToken] == null) {
+            // cycle so that during a typical ttl the tokens get completely
+            // refreshed.
+            nextUpdate = System.currentTimeMillis() + ttl
+                / (currentTokens.length - 1);
+            byte[] b = new byte[20];
+            random.nextBytes(b);
+
+            SecretKey newToken = new SecretKeySpec(b, HMAC_SHA1);
+            int nextToken = currentToken + 1;
+            if (nextToken == currentTokens.length) {
+                nextToken = 0;
+            }
+            currentTokens[nextToken] = newToken;
+            currentToken = nextToken;
+            saveTokens();
+        }
+        return currentToken;
+    }
+
+    /**
+     * Stores the current set of tokens to the token file
+     */
+    private void saveTokens() {
+        FileOutputStream fout = null;
+        DataOutputStream keyOutputStream = null;
+        try {
+            File parent = tokenFile.getAbsoluteFile().getParentFile();
+            log.info("Token File {} parent {} ", tokenFile, parent);
+            if (!parent.exists()) {
+                parent.mkdirs();
+            }
+            fout = new FileOutputStream(tmpTokenFile);
+            keyOutputStream = new DataOutputStream(fout);
+            keyOutputStream.writeInt(currentToken);
+            keyOutputStream.writeLong(nextUpdate);
+            for (int i = 0; i < currentTokens.length; i++) {
+                if (currentTokens[i] == null) {
+                    keyOutputStream.writeInt(0);
+                } else {
+                    keyOutputStream.writeInt(1);
+                    byte[] b = currentTokens[i].getEncoded();
+                    keyOutputStream.writeInt(b.length);
+                    keyOutputStream.write(b);
+                }
+            }
+            keyOutputStream.close();
+            tmpTokenFile.renameTo(tokenFile);
+        } catch (IOException e) {
+            log.error("Failed to save cookie keys " + e.getMessage());
+        } finally {
+            try {
+                keyOutputStream.close();
+            } catch (Exception e) {
+            }
+            try {
+                fout.close();
+            } catch (Exception e) {
+            }
+
+        }
+    }
+
+    /**
+     * Load the current set of tokens from the token file. If reading the tokens
+     * fails or the token file does not exist, tokens will be generated on
+     * demand.
+     */
+    private void loadTokens() {
+        if (tokenFile.isFile() && tokenFile.canRead()) {
+            FileInputStream fin = null;
+            DataInputStream keyInputStream = null;
+            try {
+                fin = new FileInputStream(tokenFile);
+                keyInputStream = new DataInputStream(fin);
+                int newCurrentToken = keyInputStream.readInt();
+                long newNextUpdate = keyInputStream.readLong();
+                SecretKey[] newKeys = new SecretKey[TOKEN_BUFFER_SIZE];
+                for (int i = 0; i < newKeys.length; i++) {
+                    int isNull = keyInputStream.readInt();
+                    if (isNull == 1) {
+                        int l = keyInputStream.readInt();
+                        byte[] b = new byte[l];
+                        keyInputStream.read(b);
+                        newKeys[i] = new SecretKeySpec(b, HMAC_SHA1);
+                    } else {
+                        newKeys[i] = null;
+                    }
+                }
+
+                // assign the tokes and schedule a next update
+                nextUpdate = newNextUpdate;
+                currentToken = newCurrentToken;
+                currentTokens = newKeys;
+
+            } catch (IOException e) {
+
+                log.error("Failed to load cookie keys " + e.getMessage());
+
+            } finally {
+
+                if (keyInputStream != null) {
+                    try {
+                        keyInputStream.close();
+                    } catch (IOException e) {
+                    }
+                } else if (fin != null) {
+                    try {
+                        fin.close();
+                    } catch (IOException e) {
+                    }
+                }
+            }
+        }
+
+        // if there was a failure to read the current tokens, create new ones
+        if (currentTokens == null) {
+            currentTokens = new SecretKey[TOKEN_BUFFER_SIZE];
+            nextUpdate = System.currentTimeMillis();
+            currentToken = 0;
+        }
+    }
+
+    /**
+     * Encode a byte array.
+     *
+     * @param base
+     * @return
+     */
+    private String byteToHex(byte[] base) {
+        char[] c = new char[base.length * 2];
+        int i = 0;
+
+        for (byte b : base) {
+            int j = b;
+            j = j + 128;
+            c[i++] = TOHEX[j / 0x10];
+            c[i++] = TOHEX[j % 0x10];
+        }
+        return new String(c);
+    }
+}
\ No newline at end of file
diff --git a/src/main/resources/OSGI-INF/metatype/metatype.properties b/src/main/resources/OSGI-INF/metatype/metatype.properties
new file mode 100644
index 0000000..686bd82
--- /dev/null
+++ b/src/main/resources/OSGI-INF/metatype/metatype.properties
@@ -0,0 +1,82 @@
+#
+#  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.
+#
+
+#
+# This file contains localization strings for configuration labels and
+# descriptions as used in the metatype.xml descriptor generated by the
+# the Sling SCR plugin
+
+auth.form.name=Apache Sling Form Based Authentication Handler
+auth.form.description=This handler extracts a hash value from a cookie or  \
+ session attribute id and compares it to a hash generated on the server.
+
+path.name = Path
+path.description = Repository path for which this authentication handler \
+ should be used by Sling. If this is empty, the authentication handler will \
+ be disabled.
+
+form.login.form.name = Login Form
+form.login.form.description = The URL (without any context path prefix) to \
+ redirect the client to to present the login form. The default value is \
+ "/system/sling/form/login".
+
+form.auth.storage.name = Hash Storage
+form.auth.storage.description = The type of storage used to provide the \
+ authentication state. Valid values are cookie and session. The default value \
+ (cookie) also applies if any setting other than the supported values is \
+ configured.
+
+form.auth.name.name = Cookie/Attribute Name
+form.auth.name.description = The name of the Cookie or HTTP Session attribute \
+ providing the authentication state. The default value is "sling.formauth".
+
+form.credentials.name.name = Credentials Attribute
+form.credentials.name.description = The name of the SimpleCredentials \
+ attribute used  to provide the authentication data to the LoginModulePlugin. \
+ The default value is "sling.formauth".
+
+form.auth.timeout.name = Timeout
+form.auth.timeout.description = The number of minutes after which a login \
+ session times out. This value is used as the expiry time set in the \
+ authentication data. The default value is 30 minutes. If the value is set \
+ a value less than 1, the default value is used instead.
+
+form.token.file.name = Security Token File
+form.token.file.description = The name of the file used to persist the \
+ security tokens. The default value is cookie-tokens.bin. This property \
+ currently refers to a file stored in the file system. If the path is a \
+ relative path, the file is either stored in the Authentication Handler bundle \
+ private data area or - if not possible - below the location indicated by the \
+ sling.home framework property or - if sling.home is not set - the current \
+ working directory. In the future this file may be store in the JCR Repository \
+ to support clustering scenarios.
+
+service.ranking.name = Ranking
+service.ranking.description = The relative ranking of this service.
+
+form.use.include.name = Include Form
+form.use.include.description = If true, this authentication handler will attempt \
+ to include a Servlet resource at the login form path. If false, a redirect will \
+ be used instead.
+
+form.onexpire.login.name = On Login Expire, Re-login
+form.onexpire.login.description = If true, when the form login expires the user \
+ will be prompted to re-login. If false they become an anonymous user. The default \
+ is false.
+ 
\ No newline at end of file
diff --git a/src/main/resources/org/apache/sling/formauth/impl/login.html b/src/main/resources/org/apache/sling/formauth/impl/login.html
new file mode 100644
index 0000000..36d924d
--- /dev/null
+++ b/src/main/resources/org/apache/sling/formauth/impl/login.html
@@ -0,0 +1,93 @@
+<!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"
+>
+<head>
+   <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+   <title>Login</title>
+   
+   <style type="text/css">
+   body {
+       font-family: Verdana, Arial, Helvetica, sans-serif;
+       font-size: 10px;
+       color: black;
+       background-color: white;
+   }
+   
+   #main {
+       border: 1px solid gray;
+       margin-top: 25%;
+       margin-left: 25%;
+       width: 400px;
+       padding: 10px;
+   }
+   
+   #loginform {
+       padding: 0px;
+       margin: 0px;
+   }
+   
+   #err {
+       color: red;
+   }
+   </style>
+   
+</head>
+
+<body>
+
+<div id="main"><!-- Login Form -->
+<h3>Login:</h3>
+<form id="loginform" method="POST" action="${requestContextPath}/j_security_check"
+        enctype="multipart/form-data" accept-charset="UTF-8">
+
+   <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">Username:</label>
+   </div>
+   <div>
+      <input id="j_username" name="j_username" type="text" />
+   </div>
+   
+   
+   <div>
+      <label for="j_password" accesskey="p">Password:</label>
+   </div>
+   <div>
+      <input id="j_password" name="j_password" type="password" />
+   </div>
+
+   <div class="buttongroup">
+      <button id="login" accesskey="l" class="form-button" type="submit">Login</button>
+   </div>
+</form>
+</div>
+
+</body>
+</html>
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..2db1617
--- /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.name()));
+    }
+
+    public void test_INVALID_CREDENTIALS() {
+        assertEquals(FormReason.INVALID_CREDENTIALS,
+            FormReason.valueOf(FormReason.INVALID_CREDENTIALS.name()));
+    }
+
+    public void test_INVALID() {
+        try {
+            FormReason.valueOf("INVALID");
+            fail("unexpected result getting value of an invalid constant");
+        } catch (IllegalArgumentException iae) {
+            // expected
+        }
+    }
+}
diff --git a/src/test/java/org/apache/sling/formauth/impl/FormAuthenticationHandlerTest.java b/src/test/java/org/apache/sling/formauth/impl/FormAuthenticationHandlerTest.java
new file mode 100644
index 0000000..c0f6cff
--- /dev/null
+++ b/src/test/java/org/apache/sling/formauth/impl/FormAuthenticationHandlerTest.java
@@ -0,0 +1,147 @@
+/*
+ * 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.impl;
+
+import java.io.File;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.formauth.impl.FormAuthenticationHandler;
+import org.hamcrest.Description;
+import org.hamcrest.text.StringStartsWith;
+import org.jmock.Expectations;
+import org.jmock.Mockery;
+import org.jmock.api.Action;
+import org.jmock.api.Invocation;
+import org.osgi.framework.BundleContext;
+
+public class FormAuthenticationHandlerTest extends TestCase {
+
+    public void test_getTokenFile() {
+        final File root = new File("bundle999").getAbsoluteFile();
+        final SlingHomeAction slingHome = new SlingHomeAction();
+        slingHome.setSlingHome(new File("sling").getAbsolutePath());
+
+        Mockery context = new Mockery();
+        final BundleContext bundleContext = context.mock(BundleContext.class);
+
+        context.checking(new Expectations() {
+            {
+                // mock access to sling.home framework property
+                allowing(bundleContext).getProperty("sling.home");
+                will(slingHome);
+
+                // mock no data file support with file names starting with sl
+                allowing(bundleContext).getDataFile(
+                    with(new StringStartsWith("sl")));
+                will(returnValue(null));
+
+                // mock data file support for any other name
+                allowing(bundleContext).getDataFile(with(any(String.class)));
+                will(new RVA(root));
+            }
+        });
+
+        final FormAuthenticationHandler handler = new FormAuthenticationHandler();
+
+        // test files relative to bundle context
+        File relFile0 = handler.getTokenFile("", bundleContext);
+        assertEquals(root, relFile0);
+
+        String relName1 = "rel/path";
+        File relFile1 = handler.getTokenFile(relName1, bundleContext);
+        assertEquals(new File(root, relName1), relFile1);
+
+        // test file relative to sling.home if no data file support
+        String relName2 = "sl/rel_to_sling.home";
+        File relFile2 = handler.getTokenFile(relName2, bundleContext);
+        assertEquals(new File(slingHome.getSlingHome(), relName2), relFile2);
+
+        // test file relative to current working directory
+        String relName3 = "sl/test";
+        slingHome.setSlingHome(null);
+        File relFile3 = handler.getTokenFile(relName3, bundleContext);
+        assertEquals(new File(relName3).getAbsoluteFile(), relFile3);
+
+        // test absolute file return
+        File absFile = new File("test").getAbsoluteFile();
+        File absFile0 = handler.getTokenFile(absFile.getPath(), bundleContext);
+        assertEquals(absFile, absFile0);
+    }
+
+    public void test_getUserid() {
+        final FormAuthenticationHandler handler = new FormAuthenticationHandler();
+        assertEquals(null, handler.getUserId(null));
+        assertEquals(null, handler.getUserId(""));
+        assertEquals(null, handler.getUserId("field0"));
+        assertEquals(null, handler.getUserId("field0@field1"));
+        assertEquals("field3", handler.getUserId("field0@field1@field3"));
+        assertEquals(null, handler.getUserId("field0@field1@field3@field4"));
+    }
+
+    /**
+     * The <code>RVA</code> action returns a file relative to some root file as
+     * requested by the first parameter of the invocation, expecting the first
+     * parameter to be a string.
+     */
+    private static class RVA implements Action {
+
+        private final File root;
+
+        RVA(final File root) {
+            this.root = root;
+        }
+
+        public Object invoke(Invocation invocation) throws Throwable {
+            String data = (String) invocation.getParameter(0);
+            if (data.startsWith("/")) {
+                data = data.substring(1);
+            }
+            return new File(root, data);
+        }
+
+        public void describeTo(Description description) {
+            description.appendText("returns new File(root, arg0)");
+        }
+    }
+
+    /**
+     * The <code>SlingHomeAction</code> action returns the current value of the
+     * <code>slingHome</code> field on all invocations
+     */
+    private static class SlingHomeAction implements Action {
+        private String slingHome;
+
+        public void setSlingHome(String slingHome) {
+            this.slingHome = slingHome;
+        }
+
+        public String getSlingHome() {
+            return slingHome;
+        }
+
+        public Object invoke(Invocation invocation) throws Throwable {
+            return slingHome;
+        }
+
+        public void describeTo(Description description) {
+            description.appendText("returns " + slingHome);
+        }
+    }
+}