You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by fm...@apache.org on 2010/02/08 16:08:38 UTC
svn commit: r907677 - in /sling/trunk/bundles/extensions/formauth: ./ src/
src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/
src/main/java/org/apache/sling/ src/main/java/org/apache/sling/formauth/
src/main/resources/ src/main/resou...
Author: fmeschbe
Date: Mon Feb 8 15:08:38 2010
New Revision: 907677
URL: http://svn.apache.org/viewvc?rev=907677&view=rev
Log:
SLING-1116 Initial Version based on Eric Norman's patch (thanks alot)
Added:
sling/trunk/bundles/extensions/formauth/ (with props)
sling/trunk/bundles/extensions/formauth/README.txt (with props)
sling/trunk/bundles/extensions/formauth/pom.xml (with props)
sling/trunk/bundles/extensions/formauth/src/
sling/trunk/bundles/extensions/formauth/src/main/
sling/trunk/bundles/extensions/formauth/src/main/java/
sling/trunk/bundles/extensions/formauth/src/main/java/org/
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java (with props)
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java (with props)
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java (with props)
sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java (with props)
sling/trunk/bundles/extensions/formauth/src/main/resources/
sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/
sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/
sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties (with props)
sling/trunk/bundles/extensions/formauth/src/main/resources/login.html (with props)
sling/trunk/bundles/extensions/formauth/src/test/
sling/trunk/bundles/extensions/formauth/src/test/java/
sling/trunk/bundles/extensions/formauth/src/test/java/org/
sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/
sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/
sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/
sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java (with props)
Propchange: sling/trunk/bundles/extensions/formauth/
------------------------------------------------------------------------------
--- svn:ignore (added)
+++ svn:ignore Mon Feb 8 15:08:38 2010
@@ -0,0 +1,4 @@
+.classpath
+.project
+target
+.settings
Added: sling/trunk/bundles/extensions/formauth/README.txt
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/README.txt?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/README.txt (added)
+++ sling/trunk/bundles/extensions/formauth/README.txt Mon Feb 8 15:08:38 2010
@@ -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.
+
Propchange: sling/trunk/bundles/extensions/formauth/README.txt
------------------------------------------------------------------------------
svn:eol-style = native
Added: sling/trunk/bundles/extensions/formauth/pom.xml
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/pom.xml?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/pom.xml (added)
+++ sling/trunk/bundles/extensions/formauth/pom.xml Mon Feb 8 15:08:38 2010
@@ -0,0 +1,177 @@
+<?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>8</version>
+ <relativePath>../../../parent/pom.xml</relativePath>
+ </parent>
+
+ <artifactId>org.apache.sling.formauth</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/extensions/formauth</connection>
+ <developerConnection>scm:svn:https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/formauth</developerConnection>
+ <url>http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth</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>
+ <Private-Package>
+ org.apache.sling.formauth.*
+ </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.formauth
+ </excludePackageNames>
+ </configuration>
+ </plugin>
+ </plugins>
+ </reporting>
+ <dependencies>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.api</artifactId>
+ <version>2.0.8</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.commons.osgi</artifactId>
+ <version>2.0.4-incubator</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.engine</artifactId>
+ <version>2.0.6</version>
+ <scope>provided</scope>
+ </dependency>
+ <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>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>
+
+ <dependency>
+ <groupId>org.apache.sling</groupId>
+ <artifactId>org.apache.sling.jcr.jackrabbit.server</artifactId>
+ <version>2.0.5-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>
+ <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>
Propchange: sling/trunk/bundles/extensions/formauth/pom.xml
------------------------------------------------------------------------------
svn:eol-style = native
Added: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java Mon Feb 8 15:08:38 2010
@@ -0,0 +1,127 @@
+/*
+ * 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 java.io.IOException;
+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.
+ *
+ * @scr.component metatype="no"
+ * @scr.service interface="javax.servlet.Servlet"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ * @scr.property name="service.description"
+ * value="Default Login Form for Form Based Authentication"
+ */
+@SuppressWarnings("serial")
+public class AuthenticationFormServlet extends HttpServlet {
+
+ /**
+ * @scr.property name="sling.servlet.paths"
+ */
+ static final String SERVLET_PATH = "/system/sling/form/login";
+
+ /**
+ * @scr.property name="sling.auth.requirements"
+ */
+ @SuppressWarnings("unused")
+ private static final String AUTH_REQUIREMENT = "-" + SERVLET_PATH;
+
+ private volatile String rawForm;
+
+ @Override
+ protected void doGet(HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+
+ // 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("Pragma", "no-cache");
+ response.setHeader("Expires", "0");
+
+ // send the form and flush
+ response.getWriter().print(getForm(request));
+ response.flushBuffer();
+ }
+
+ @Override
+ protected void doPost(HttpServletRequest request,
+ HttpServletResponse response) throws ServletException, IOException {
+ super.doPost(request, response);
+ }
+
+ private String getForm(final HttpServletRequest request) throws IOException {
+
+ String resource = (String) request.getAttribute(Authenticator.LOGIN_RESOURCE);
+ if (resource == null) {
+ resource = request.getParameter(Authenticator.LOGIN_RESOURCE);
+ if (resource == null) {
+ resource = "/";
+ }
+ }
+
+ return getRawForm().replace("${resource}", resource);
+ }
+
+ private String getRawForm() throws IOException {
+ if (rawForm == null) {
+ InputStream ins = null;
+ try {
+ ins = getClass().getResourceAsStream("/login.html");
+ if (ins != null) {
+ StringBuilder builder = new StringBuilder();
+ Reader r = new InputStreamReader(ins, "UTF-8");
+ char[] cbuf = new char[1024];
+ int rd = 0;
+ while ((rd = r.read(cbuf)) >= 0) {
+ builder.append(cbuf, 0, rd);
+ }
+
+ rawForm = builder.toString();
+ }
+ } finally {
+ if (ins != null) {
+ try {
+ ins.close();
+ } catch (IOException ignore) {
+ }
+ }
+ }
+
+ if (rawForm == null) {
+ throw new IOException("Failed reading form template");
+ }
+ }
+
+ return rawForm;
+ }
+}
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev Url
Added: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java Mon Feb 8 15:08:38 2010
@@ -0,0 +1,751 @@
+/*
+ * 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 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.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.commons.auth.Authenticator;
+import org.apache.sling.commons.auth.spi.AuthenticationFeedbackHandler;
+import org.apache.sling.commons.auth.spi.AuthenticationHandler;
+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.osgi.framework.BundleContext;
+import org.osgi.service.component.ComponentContext;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * The <code>CookieAuthenticationHandler</code> class implements the
+ * authorization steps based on a cookie.
+ *
+ * @scr.component immediate="false" label="%auth.form.name"
+ * description="%auth.form.description"
+ * @scr.property name="service.description"
+ * value="Cookie Based Authentication Handler"
+ * @scr.property name="service.vendor" value="The Apache Software Foundation"
+ * @scr.property nameRef="AuthenticationHandler.PATH_PROPERTY" value="/"
+ * @scr.service
+ */
+public class FormAuthenticationHandler implements AuthenticationHandler,
+ AuthenticationFeedbackHandler {
+
+ /**
+ * The request parameter causing a 401/UNAUTHORIZED status to be sent back
+ * in the {@link #authenticate(HttpServletRequest, HttpServletResponse)}
+ * method if no credentials are present in the request (value is
+ * "sling:authRequestLogin").
+ *
+ * @see #requestCredentials(HttpServletRequest, HttpServletResponse)
+ */
+ static final String REQUEST_LOGIN_PARAMETER = "sling:authRequestLogin";
+
+ /**
+ * 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";
+
+ /**
+ * 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 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;
+
+ private String attrCookieAuthData;
+
+ private TokenStore tokenStore;
+
+ /**
+ * 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 && tokenStore.isValid(authData)) {
+ info = createAuthInfo(authData);
+ }
+ }
+
+ return info;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.apache.sling.commons.auth.spi.AuthenticationHandler#requestCredentials
+ * (javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse)
+ */
+ public boolean requestCredentials(HttpServletRequest request,
+ HttpServletResponse response) throws IOException {
+
+ // 0. ignore this handler if an authentication handler is requested
+ if (ignoreRequestCredentials(request)) {
+ 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();
+ }
+ }
+
+ 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"));
+
+ final String target = targetBuilder.toString();
+ try {
+ response.sendRedirect(target);
+ } catch (IOException e) {
+ log.error("Failed to redirect to the page: " + target, e);
+ }
+
+ return true;
+ }
+
+ /*
+ * (non-Javadoc)
+ * @see
+ * org.apache.sling.commons.auth.spi.AuthenticationHandler#dropCredentials
+ * (javax.servlet.http.HttpServletRequest,
+ * javax.servlet.http.HttpServletResponse)
+ */
+ 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
+
+ /**
+ * 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) {
+ authStorage.clear(request, response);
+ }
+
+ /**
+ * 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) {
+
+ // 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);
+ }
+ }
+
+ 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) {
+ try {
+ response.sendRedirect(resource);
+ } catch (IOException ioe) {
+ }
+ return true;
+ }
+
+ }
+
+ // no redirect
+ return false;
+ }
+
+ @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(HttpServletRequest request) {
+ final String requestLogin = request.getParameter(REQUEST_LOGIN_PARAMETER);
+ return requestLogin != null
+ && !HttpServletRequest.FORM_AUTH.equals(requestLogin);
+ }
+
+ // --------- 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 && user.length() > 0 && pwd != null) {
+ info = new AuthenticationInfo(HttpServletRequest.FORM_AUTH,
+ user, pwd.toCharArray());
+ }
+ }
+
+ return info;
+ }
+
+ private AuthenticationInfo createAuthInfo(final String authData) {
+ final String userId = getUserId(authData);
+ if (userId == null) {
+ return null;
+ }
+
+ final SimpleCredentials cookieAuthCredentials = new SimpleCredentials(
+ userId, new char[0]);
+ cookieAuthCredentials.setAttribute(attrCookieAuthData, authData);
+
+ final AuthenticationInfo info = new AuthenticationInfo(
+ HttpServletRequest.FORM_AUTH, userId);
+ info.setCredentials(cookieAuthCredentials);
+
+ return info;
+ }
+
+ // ---------- 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);
+ this.tokenStore = new TokenStore(tokenFile, sessionTimeout);
+ }
+
+ /**
+ * 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
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev Url
Added: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java Mon Feb 8 15:08:38 2010
@@ -0,0 +1,106 @@
+/*
+ * 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 java.security.Principal;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.Credentials;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.security.auth.callback.CallbackHandler;
+import org.apache.sling.jcr.jackrabbit.server.security.AuthenticationPlugin;
+import org.apache.sling.jcr.jackrabbit.server.security.LoginModulePlugin;
+
+final class FormLoginModulePlugin implements LoginModulePlugin {
+
+ private final FormAuthenticationHandler authHandler;
+
+ 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 a simple <code>Principal</code> just providing the user id
+ * contained in the <code>SimpleCredentials</code> object. If the
+ * credentials is not a <code>SimpleCredentials</code> instance,
+ * <code>null</code> is returned.
+ */
+ public Principal getPrincipal(final Credentials credentials) {
+ if (credentials instanceof SimpleCredentials) {
+ return new Principal() {
+ public String getName() {
+ return ((SimpleCredentials) credentials).getUserID();
+ }
+ };
+ }
+ return null;
+ }
+
+ /**
+ * This implementation does nothing.
+ */
+ @SuppressWarnings("unchecked")
+ public void addPrincipals(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
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev Url
Added: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java Mon Feb 8 15:08:38 2010
@@ -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;
+
+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
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: sling/trunk/bundles/extensions/formauth/src/main/java/org/apache/sling/formauth/TokenStore.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev Url
Added: sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties Mon Feb 8 15:08:38 2010
@@ -0,0 +1,68 @@
+#
+# 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.
Propchange: sling/trunk/bundles/extensions/formauth/src/main/resources/OSGI-INF/metatype/metatype.properties
------------------------------------------------------------------------------
svn:eol-style = native
Added: sling/trunk/bundles/extensions/formauth/src/main/resources/login.html
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/main/resources/login.html?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/main/resources/login.html (added)
+++ sling/trunk/bundles/extensions/formauth/src/main/resources/login.html Mon Feb 8 15:08:38 2010
@@ -0,0 +1,65 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<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;
+ }
+ </style>
+
+</head>
+
+<body>
+
+<div id="main"><!-- Login Form -->
+<h3>Login:</h3>
+<form id="loginform" method="POST" action="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>
+ <label for="j_username" accesskey="u">User ID:</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>
Propchange: sling/trunk/bundles/extensions/formauth/src/main/resources/login.html
------------------------------------------------------------------------------
svn:eol-style = native
Added: sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java?rev=907677&view=auto
==============================================================================
--- sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java (added)
+++ sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java Mon Feb 8 15:08:38 2010
@@ -0,0 +1,146 @@
+/*
+ * 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 java.io.File;
+
+import junit.framework.TestCase;
+
+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);
+ }
+ }
+}
Propchange: sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: sling/trunk/bundles/extensions/formauth/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision Rev Url