This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to annotated tag org.apache.sling.auth.form-1.0.0 in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-auth-form.git
commit f467e0e07080495017136469d0dac42c7df2266e Author: Felix Meschberger <[email protected]> AuthorDate: Mon Feb 8 15:08:38 2010 +0000 SLING-1116 Initial Version based on Eric Norman's patch (thanks alot) git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/bundles/extensions/formauth@907677 13f79535-47bb-0310-9956-ffa450edef68 --- README.txt | 30 + pom.xml | 177 +++++ .../sling/formauth/AuthenticationFormServlet.java | 127 ++++ .../sling/formauth/FormAuthenticationHandler.java | 751 +++++++++++++++++++++ .../sling/formauth/FormLoginModulePlugin.java | 106 +++ .../java/org/apache/sling/formauth/TokenStore.java | 380 +++++++++++ .../OSGI-INF/metatype/metatype.properties | 68 ++ src/main/resources/login.html | 65 ++ .../formauth/FormAuthenticationHandlerTest.java | 146 ++++ 9 files changed, 1850 insertions(+) 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..5732e73 --- /dev/null +++ b/pom.xml @@ -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> diff --git a/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java b/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java new file mode 100644 index 0000000..7c9f789 --- /dev/null +++ b/src/main/java/org/apache/sling/formauth/AuthenticationFormServlet.java @@ -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; + } +} diff --git a/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java b/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java new file mode 100644 index 0000000..c135cc4 --- /dev/null +++ b/src/main/java/org/apache/sling/formauth/FormAuthenticationHandler.java @@ -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 diff --git a/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java b/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java new file mode 100644 index 0000000..e4b60dd --- /dev/null +++ b/src/main/java/org/apache/sling/formauth/FormLoginModulePlugin.java @@ -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 diff --git a/src/main/java/org/apache/sling/formauth/TokenStore.java b/src/main/java/org/apache/sling/formauth/TokenStore.java new file mode 100644 index 0000000..390f1b2 --- /dev/null +++ b/src/main/java/org/apache/sling/formauth/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; + +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..1dc9616 --- /dev/null +++ b/src/main/resources/OSGI-INF/metatype/metatype.properties @@ -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. diff --git a/src/main/resources/login.html b/src/main/resources/login.html new file mode 100644 index 0000000..2d3c7a0 --- /dev/null +++ b/src/main/resources/login.html @@ -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> diff --git a/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java b/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java new file mode 100644 index 0000000..31e8e21 --- /dev/null +++ b/src/test/java/org/apache/sling/formauth/FormAuthenticationHandlerTest.java @@ -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); + } + } +} -- To stop receiving notification emails like this one, please contact "[email protected]" <[email protected]>.
