| /* |
| * 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.auth.form.impl; |
| |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.UnsupportedEncodingException; |
| import java.net.URLEncoder; |
| import java.security.InvalidKeyException; |
| import java.security.NoSuchAlgorithmException; |
| import java.util.Dictionary; |
| |
| import javax.jcr.Credentials; |
| import javax.jcr.SimpleCredentials; |
| import javax.servlet.Servlet; |
| import javax.servlet.ServletException; |
| import javax.servlet.http.Cookie; |
| import javax.servlet.http.HttpServletRequest; |
| import javax.servlet.http.HttpServletResponse; |
| import javax.servlet.http.HttpSession; |
| |
| import org.apache.commons.codec.binary.Base64; |
| import org.apache.commons.lang.StringUtils; |
| import org.apache.sling.api.auth.Authenticator; |
| import org.apache.sling.api.resource.LoginException; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceResolverFactory; |
| import org.apache.sling.auth.core.spi.AbstractAuthenticationHandler; |
| import org.apache.sling.auth.core.spi.AuthenticationInfo; |
| import org.apache.sling.auth.core.spi.DefaultAuthenticationFeedbackHandler; |
| import org.apache.sling.auth.form.FormReason; |
| import org.apache.sling.commons.osgi.OsgiUtil; |
| import org.osgi.framework.BundleContext; |
| import org.osgi.framework.Constants; |
| import org.osgi.framework.ServiceRegistration; |
| import org.osgi.service.component.ComponentContext; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * The <code>FormAuthenticationHandler</code> class implements the authorization |
| * steps based on a cookie. |
| * |
| * @scr.component immediate="false" label="%auth.form.name" |
| * description="%auth.form.description" |
| * name="org.apache.sling.auth.form.FormAuthenticationHandler" |
| * @scr.property name="service.description" |
| * value="Apache Sling Form Based Authentication Handler" |
| * @scr.property name="service.vendor" value="The Apache Software Foundation" |
| * @scr.property nameRef= |
| * "org.apache.sling.auth.core.spi.AuthenticationHandler.PATH_PROPERTY" |
| * values.0="/" |
| * @scr.property nameRef= |
| * "org.apache.sling.auth.core.spi.AuthenticationHandler.TYPE_PROPERTY" |
| * valueRef="javax.servlet.http.HttpServletRequest.FORM_AUTH" |
| * private="true" |
| * @scr.service |
| */ |
| public class FormAuthenticationHandler extends AbstractAuthenticationHandler { |
| |
| /** |
| * The name of the parameter providing the login form URL. |
| * |
| * @scr.property valueRef="AuthenticationFormServlet.SERVLET_PATH" |
| */ |
| private static final String PAR_LOGIN_FORM = "form.login.form"; |
| |
| /** |
| * @scr.property valueRef="DEFAULT_AUTH_STORAGE" options "cookie"="Cookie" |
| * "session"="Session Attribute" |
| */ |
| private static final String PAR_AUTH_STORAGE = "form.auth.storage"; |
| |
| /** |
| * The value of the {@link #PAR_AUTH_STORAGE} parameter indicating the use |
| * of a Cookie to store the authentication data. |
| */ |
| private static final String AUTH_STORAGE_COOKIE = "cookie"; |
| |
| /** |
| * The value of the {@link #PAR_AUTH_STORAGE} parameter indicating the use |
| * of a session attribute to store the authentication data. |
| */ |
| private static final String AUTH_STORAGE_SESSION_ATTRIBUTE = "session"; |
| |
| /** |
| * To be used to determine if the auth has value comes from a cookie or from |
| * a session attribute. |
| */ |
| private static final String DEFAULT_AUTH_STORAGE = AUTH_STORAGE_COOKIE; |
| |
| /** |
| * The name of the configuration parameter providing the Cookie or session |
| * attribute name. |
| * |
| * @scr.property valueRef="DEFAULT_AUTH_NAME" |
| */ |
| private static final String PAR_AUTH_NAME = "form.auth.name"; |
| |
| /** |
| * The default Cookie or session attribute name |
| * |
| * @see #PAR_AUTH_NAME |
| */ |
| private static final String DEFAULT_AUTH_NAME = "sling.formauth"; |
| |
| /** |
| * This is the name of the SimpleCredentials attribute that holds the auth |
| * info extracted from the cookie value. |
| * |
| * @scr.property valueRef="DEFAULT_CREDENTIALS_ATTRIBUTE_NAME" |
| */ |
| private static final String PAR_CREDENTIALS_ATTRIBUTE_NAME = "form.credentials.name"; |
| |
| /** |
| * Default value for the {@link #PAR_CREDENTIALS_ATTRIBUTE_NAME} property |
| */ |
| private static final String DEFAULT_CREDENTIALS_ATTRIBUTE_NAME = DEFAULT_AUTH_NAME; |
| |
| /** |
| * The number of minutes after which a login session times out. This value |
| * is used as the expiry time set in the authentication data. |
| * |
| * @scr.property type="Integer" valueRef="DEFAULT_AUTH_TIMEOUT" |
| */ |
| public static final String PAR_AUTH_TIMEOUT = "form.auth.timeout"; |
| |
| /** |
| * The default authentication data time out value. |
| * |
| * @see #PAR_AUTH_TIMEOUT |
| */ |
| private static final int DEFAULT_AUTH_TIMEOUT = 30; |
| |
| /** |
| * The name of the file used to persist the security tokens |
| * |
| * @scr.property valueRef="DEFAULT_TOKEN_FILE" |
| */ |
| private static final String PAR_TOKEN_FILE = "form.token.file"; |
| |
| private static final String DEFAULT_TOKEN_FILE = "cookie-tokens.bin"; |
| |
| /** |
| * Whether to redirect to the login form or simple do an include. |
| * |
| * @scr.property type="Boolean" valueRef="DEFAULT_INCLUDE_FORM" |
| */ |
| public static final String PAR_INCLUDE_FORM = "form.use.include"; |
| |
| /** |
| * The default include value. |
| * |
| * @see #PAR_INCLUDE_FORM |
| */ |
| private static final boolean DEFAULT_INCLUDE_FORM = false; |
| |
| |
| /** |
| * Whether to present a login form when a users cookie expires, the default |
| * is not to present the form. |
| * |
| * @scr.property type="Boolean" valueRef="DEFAULT_LOGIN_AFTER_EXPIRE" |
| */ |
| private static final String PAR_LOGIN_AFTER_EXPIRE = "form.onexpire.login"; |
| |
| /** |
| * The default login after expire of a cookie. |
| * |
| * @see #PAR_LOGIN_AFTER_EXPIRE |
| */ |
| private static final boolean DEFAULT_LOGIN_AFTER_EXPIRE = false; |
| |
| /** |
| * The request method required for user name and password submission by the |
| * form (value is "POST"). |
| */ |
| private static final String REQUEST_METHOD = "POST"; |
| |
| /** |
| * The last segment of the request URL for the user name and password |
| * submission by the form (value is "/j_security_check"). |
| * <p> |
| * This name is derived from the prescription in the Servlet API 2.4 |
| * Specification, Section SRV.12.5.3.1 Login Form Notes: <i>In order for the |
| * authentication to proceeed appropriately, the action of the login form |
| * must always be set to <code>j_security_check</code>.</i> |
| */ |
| private static final String REQUEST_URL_SUFFIX = "/j_security_check"; |
| |
| /** |
| * The name of the form submission parameter providing the name of the user |
| * to authenticate (value is "j_username"). |
| * <p> |
| * This name is prescribed by the Servlet API 2.4 Specification, Section |
| * SRV.12.5.3 Form Based Authentication. |
| */ |
| private static final String PAR_J_USERNAME = "j_username"; |
| |
| /** |
| * The name of the form submission parameter providing the password of the |
| * user to authenticate (value is "j_password"). |
| * <p> |
| * This name is prescribed by the Servlet API 2.4 Specification, Section |
| * SRV.12.5.3 Form Based Authentication. |
| */ |
| private static final String PAR_J_PASSWORD = "j_password"; |
| |
| /** |
| * The name of the form submission parameter indicating that the submitted |
| * username and password should just be checked and a status code be set for |
| * success (200/OK) or failure (403/FORBIDDEN). |
| */ |
| private static final String PAR_J_VALIDATE = "j_validate"; |
| |
| /** |
| * The name of the request parameter indicating to the login form why the |
| * form is being rendered. If this parameter is not set the form is called |
| * for the first time and the implied reason is that the authenticator just |
| * requests credentials. Otherwise the parameter is set to a |
| * {@link FormReason} value. |
| */ |
| static final String PAR_J_REASON = "j_reason"; |
| |
| /** |
| * The service ranking property. |
| * |
| * @scr.property type="Integer" value="0" private="false" |
| */ |
| @SuppressWarnings("unused") |
| private static final String PAR_SERVICE_RANKING = Constants.SERVICE_RANKING; |
| |
| /** |
| * The factor to convert minute numbers into milliseconds used internally |
| */ |
| private static final long MINUTES = 60L * 1000L; |
| |
| /** default log */ |
| private final Logger log = LoggerFactory.getLogger(getClass()); |
| |
| private AuthenticationStorage authStorage; |
| |
| private String loginForm; |
| |
| /** |
| * The timeout of a login session in milliseconds, converted from the |
| * configuration property {@link #PAR_AUTH_TIMEOUT} by multiplying with |
| * {@link #MINUTES}. |
| */ |
| private long sessionTimeout; |
| |
| /** |
| * The name of the credentials attribute which is set to the cookie data |
| * to validate. |
| */ |
| private String attrCookieAuthData; |
| |
| /** |
| * The {@link TokenStore} used to persist and check authentication data |
| */ |
| private TokenStore tokenStore; |
| |
| /** |
| * The {@link FormLoginModulePlugin} service registration created when |
| * this authentication handler is registered. If the login module plugin |
| * cannot be created this field is set to <code>null</code>. |
| */ |
| private ServiceRegistration loginModule; |
| |
| /** |
| * If true, the handler will attempt to include the login form instead of |
| * doing a redirect. |
| */ |
| private boolean includeLoginForm; |
| |
| /** |
| * The resource resolver factory used to resolve the login form as a resource |
| * |
| * @scr.reference policy="dynamic" cardinality="0..1" |
| */ |
| private ResourceResolverFactory resourceResolverFactory; |
| |
| /** |
| * If true the login form will be presented when the token expires. |
| */ |
| private boolean loginAfterExpire; |
| |
| |
| /** |
| * Extracts cookie/session based credentials from the request. Returns |
| * <code>null</code> if the handler assumes HTTP Basic authentication would |
| * be more appropriate, if no form fields are present in the request and if |
| * the secure user data is not present either in the cookie or an HTTP |
| * Session. |
| */ |
| public AuthenticationInfo extractCredentials(HttpServletRequest request, |
| HttpServletResponse response) { |
| |
| AuthenticationInfo info = null; |
| |
| // 1. try credentials from POST'ed request parameters |
| info = this.extractRequestParameterAuthentication(request); |
| |
| // 2. try credentials from the cookie or session |
| if (info == null) { |
| String authData = authStorage.extractAuthenticationInfo(request); |
| if (authData != null) { |
| if (tokenStore.isValid(authData)) { |
| info = createAuthInfo(authData); |
| } else { |
| if (this.loginAfterExpire) { |
| // signal the requestCredentials method a previous login failure |
| request.setAttribute(PAR_J_REASON, FormReason.TIMEOUT); |
| info = AuthenticationInfo.FAIL_AUTH; |
| } |
| // clear the cookie, its invalid and we should get rid of it so that the invalid cookie |
| // isn't present on the authN operation. |
| authStorage.clear(request, response); |
| } |
| } |
| } |
| |
| return info; |
| } |
| |
| /** |
| * Unless the <code>sling:authRequestLogin</code> to anything other than |
| * <code>Form</code> this method either sends back a 403/FORBIDDEN response |
| * if the <code>j_verify</code> parameter is set to <code>true</code> or |
| * redirects to the login form to ask for credentials. |
| * <p> |
| * This method assumes the <code>j_verify</code> request parameter to only |
| * be set in the initial username/password submission through the login |
| * form. No further checks are applied, though, before sending back the |
| * 403/FORBIDDEN response. |
| */ |
| public boolean requestCredentials(HttpServletRequest request, |
| HttpServletResponse response) throws IOException { |
| |
| // 0. ignore this handler if an authentication handler is requested |
| if (ignoreRequestCredentials(request)) { |
| // consider this handler is not used |
| return false; |
| } |
| |
| // 1. check whether we short cut for a failed log in with validation |
| if (isValidateRequest(request)) { |
| try { |
| response.setStatus(403); |
| response.flushBuffer(); |
| } catch (IOException ioe) { |
| log.error("Failed to send 403/FORBIDDEN response", ioe); |
| } |
| |
| // consider credentials requested |
| return true; |
| } |
| |
| String resource = getLoginResource(request, null); |
| if (resource == null) { |
| resource = request.getContextPath() + request.getPathInfo(); |
| request.setAttribute(Authenticator.LOGIN_RESOURCE, resource); |
| } |
| |
| if (includeLoginForm && (resourceResolverFactory != null)) { |
| ResourceResolver resourceResolver = null; |
| try { |
| resourceResolver = resourceResolverFactory.getAdministrativeResourceResolver(null); |
| Resource loginFormResource = resourceResolver.resolve(loginForm); |
| Servlet loginFormServlet = loginFormResource.adaptTo(Servlet.class); |
| if (loginFormServlet != null) { |
| try { |
| loginFormServlet.service(request, response); |
| return true; |
| } catch (ServletException e) { |
| log.error("Failed to include the form: " + loginForm, e); |
| } |
| } |
| } catch (LoginException e) { |
| log.error("Unable to get a resource resolver to include for the login resource. Will redirect instead."); |
| } finally { |
| if (resourceResolver != null) { |
| resourceResolver.close(); |
| } |
| } |
| } |
| |
| // prepare the login form redirection target |
| final StringBuilder targetBuilder = new StringBuilder(); |
| targetBuilder.append(request.getContextPath()); |
| targetBuilder.append(loginForm); |
| |
| // append originally requested resource (for redirect after login) |
| char parSep = '?'; |
| |
| if (resource != null) { |
| targetBuilder.append(parSep).append(Authenticator.LOGIN_RESOURCE); |
| targetBuilder.append("=").append( |
| URLEncoder.encode(resource, "UTF-8")); |
| parSep = '&'; |
| } |
| |
| // append indication of previous login failure |
| if (request.getAttribute(PAR_J_REASON) != null) { |
| final Object jReason = request.getAttribute(PAR_J_REASON); |
| @SuppressWarnings("unchecked") |
| final String reason = (jReason instanceof Enum) |
| ? ((Enum) jReason).name() |
| : jReason.toString(); |
| targetBuilder.append(parSep).append(PAR_J_REASON); |
| targetBuilder.append("=").append(URLEncoder.encode(reason, "UTF-8")); |
| } |
| |
| // finally redirect to the login form |
| final String target = targetBuilder.toString(); |
| try { |
| response.sendRedirect(target); |
| } catch (IOException e) { |
| log.error("Failed to redirect to the page: " + target, e); |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Clears all authentication state which might have been prepared by this |
| * authentication handler. |
| */ |
| public void dropCredentials(HttpServletRequest request, |
| HttpServletResponse response) { |
| authStorage.clear(request, response); |
| } |
| |
| // ---------- AuthenticationFeedbackHandler |
| |
| /** |
| * Called after an unsuccessful login attempt. This implementation makes |
| * sure the authentication data is removed either by removing the cookie or |
| * by remove the HTTP Session attribute. |
| */ |
| public void authenticationFailed(HttpServletRequest request, |
| HttpServletResponse response, AuthenticationInfo authInfo) { |
| |
| /* |
| * Note: This method is called if this handler provided credentials |
| * which cause a login failure |
| */ |
| |
| // clear authentication data from Cookie or Http Session |
| authStorage.clear(request, response); |
| |
| // signal the requestCredentials method a previous login failure |
| request.setAttribute(PAR_J_REASON, FormReason.INVALID_CREDENTIALS); |
| } |
| |
| /** |
| * Called after successfull login with the given authentication info. This |
| * implementation ensures the authentication data is set in either the |
| * cookie or the HTTP session with the correct security tokens. |
| * <p> |
| * If no authentication data already exists, it is created. Otherwise if the |
| * data has expired the data is updated with a new security token and a new |
| * expiry time. |
| * <p> |
| * If creating or updating the authentication data fails, it is actually |
| * removed from the cookie or the HTTP session and future requests will not |
| * be authenticated any longer. |
| */ |
| public boolean authenticationSucceeded(HttpServletRequest request, |
| HttpServletResponse response, AuthenticationInfo authInfo) { |
| |
| /* |
| * Note: This method is called if this handler provided credentials |
| * which succeeded loging into the repository |
| */ |
| |
| // ensure fresh authentication data |
| refreshAuthData(request, response, authInfo); |
| |
| final boolean result; |
| if (isValidateRequest(request)) { |
| |
| try { |
| response.setStatus(200); |
| response.flushBuffer(); |
| } catch (IOException ioe) { |
| log.error("Failed to send 200/OK response", ioe); |
| } |
| |
| // terminate request, all done |
| result = true; |
| |
| } else if (DefaultAuthenticationFeedbackHandler.handleRedirect( |
| request, response)) { |
| |
| // terminate request, all done in the default handler |
| result = false; |
| |
| } else { |
| |
| // check whether redirect is requested by the resource parameter |
| |
| final String resource = getLoginResource(request, null); |
| if (resource != null) { |
| try { |
| response.sendRedirect(resource); |
| } catch (IOException ioe) { |
| log.error("Failed to send redirect to: " + resource, ioe); |
| } |
| |
| // terminate request, all done |
| result = true; |
| } else { |
| // no redirect, hence continue processing |
| result = false; |
| } |
| |
| } |
| |
| // no redirect |
| return result; |
| } |
| |
| @Override |
| public String toString() { |
| return "Form Based Authentication Handler"; |
| } |
| |
| // --------- Force HTTP Basic Auth --------- |
| |
| /** |
| * Returns <code>true</code> if this authentication handler should ignore |
| * the call to |
| * {@link #requestCredentials(HttpServletRequest, HttpServletResponse)}. |
| * <p> |
| * This method returns <code>true</code> if the |
| * {@link #REQUEST_LOGIN_PARAMETER} is set to any value other than "Form" |
| * (HttpServletRequest.FORM_AUTH). |
| */ |
| private boolean ignoreRequestCredentials(final HttpServletRequest request) { |
| final String requestLogin = request.getParameter(REQUEST_LOGIN_PARAMETER); |
| return requestLogin != null |
| && !HttpServletRequest.FORM_AUTH.equals(requestLogin); |
| } |
| |
| /** |
| * Returns <code>true</code> if the the client just asks for validation of |
| * submitted username/password credentials. |
| * <p> |
| * This implementation returns <code>true</code> if the request parameter |
| * {@link #PAR_J_VALIDATE} is set to <code>true</code> (case-insensitve). If |
| * the request parameter is not set or to any value other than |
| * <code>true</code> this method returns <code>false</code>. |
| * |
| * @param request The request to provide the parameter to check |
| * @return <code>true</code> if the {@link #PAR_J_VALIDATE} parameter is set |
| * to <code>true</code>. |
| */ |
| private boolean isValidateRequest(final HttpServletRequest request) { |
| return "true".equalsIgnoreCase(request.getParameter(PAR_J_VALIDATE)); |
| } |
| |
| /** |
| * Ensures the authentication data is set (if not set yet) and the expiry |
| * time is prolonged (if auth data already existed). |
| * <p> |
| * This method is intended to be called in case authentication succeeded. |
| * |
| * @param request The curent request |
| * @param response The current response |
| * @param authInfo The authentication info used to successfull log in |
| */ |
| private void refreshAuthData(final HttpServletRequest request, |
| final HttpServletResponse response, |
| final AuthenticationInfo authInfo) { |
| |
| // get current authentication data, may be missing after first login |
| String authData = getCookieAuthData(authInfo); |
| |
| // check whether we have to "store" or create the data |
| final boolean refreshCookie = needsRefresh(authData, |
| this.sessionTimeout); |
| |
| // add or refresh the stored auth hash |
| if (refreshCookie) { |
| long expires = System.currentTimeMillis() + this.sessionTimeout; |
| try { |
| authData = null; |
| authData = tokenStore.encode(expires, authInfo.getUser()); |
| } catch (InvalidKeyException e) { |
| log.error(e.getMessage(), e); |
| } catch (IllegalStateException e) { |
| log.error(e.getMessage(), e); |
| } catch (UnsupportedEncodingException e) { |
| log.error(e.getMessage(), e); |
| } catch (NoSuchAlgorithmException e) { |
| log.error(e.getMessage(), e); |
| } |
| |
| if (authData != null) { |
| authStorage.set(request, response, authData); |
| } else { |
| authStorage.clear(request, response); |
| } |
| } |
| } |
| |
| // --------- Request Parameter Auth --------- |
| |
| private AuthenticationInfo extractRequestParameterAuthentication( |
| HttpServletRequest request) { |
| AuthenticationInfo info = null; |
| |
| // only consider login form parameters if this is a POST request |
| // to the j_security_check URL |
| if (REQUEST_METHOD.equals(request.getMethod()) |
| && request.getRequestURI().endsWith(REQUEST_URL_SUFFIX)) { |
| |
| String user = request.getParameter(PAR_J_USERNAME); |
| String pwd = request.getParameter(PAR_J_PASSWORD); |
| |
| if (user != null && pwd != null) { |
| info = new AuthenticationInfo(HttpServletRequest.FORM_AUTH, |
| user, pwd.toCharArray()); |
| |
| // if this request is providing form credentials, we have to |
| // make sure, that the request is redirected after successful |
| // authentication, otherwise the request may be processed |
| // as a POST request to the j_security_check page (unless |
| // the j_validate parameter is set) |
| if (getLoginResource(request, null) == null) { |
| request.setAttribute(Authenticator.LOGIN_RESOURCE, "/"); |
| } |
| } |
| } |
| |
| return info; |
| } |
| |
| private AuthenticationInfo createAuthInfo(final String authData) { |
| final String userId = getUserId(authData); |
| if (userId == null) { |
| return null; |
| } |
| |
| final AuthenticationInfo info = new AuthenticationInfo( |
| HttpServletRequest.FORM_AUTH, userId); |
| info.put(attrCookieAuthData, authData); |
| |
| return info; |
| } |
| |
| private String getCookieAuthData(final AuthenticationInfo info) { |
| Object data = info.get(attrCookieAuthData); |
| if (data instanceof String) { |
| return (String) data; |
| } |
| return null; |
| } |
| |
| // ---------- LoginModulePlugin support |
| |
| private String getCookieAuthData(final Credentials credentials) { |
| if (credentials instanceof SimpleCredentials) { |
| Object data = ((SimpleCredentials) credentials).getAttribute(attrCookieAuthData); |
| if (data instanceof String) { |
| return (String) data; |
| } |
| } |
| |
| // no SimpleCredentials or no valid attribute |
| return null; |
| } |
| |
| boolean hasAuthData(final Credentials credentials) { |
| return getCookieAuthData(credentials) != null; |
| } |
| |
| boolean isValid(final Credentials credentials) { |
| String authData = getCookieAuthData(credentials); |
| if (authData != null) { |
| return tokenStore.isValid(authData); |
| } |
| |
| // no authdata, not valid |
| return false; |
| } |
| |
| // ---------- SCR Integration ---------------------------------------------- |
| |
| /** |
| * Called by SCR to activate the authentication handler. |
| * |
| * @throws InvalidKeyException |
| * @throws NoSuchAlgorithmException |
| * @throws IllegalStateException |
| * @throws UnsupportedEncodingException |
| */ |
| protected void activate(ComponentContext componentContext) |
| throws InvalidKeyException, NoSuchAlgorithmException, |
| IllegalStateException, UnsupportedEncodingException { |
| |
| Dictionary<?, ?> properties = componentContext.getProperties(); |
| |
| this.loginForm = OsgiUtil.toString(properties.get(PAR_LOGIN_FORM), |
| AuthenticationFormServlet.SERVLET_PATH); |
| log.info("Login Form URL {}", loginForm); |
| |
| final String authName = OsgiUtil.toString( |
| properties.get(PAR_AUTH_NAME), DEFAULT_AUTH_NAME); |
| final String authStorage = OsgiUtil.toString( |
| properties.get(PAR_AUTH_STORAGE), DEFAULT_AUTH_STORAGE); |
| if (AUTH_STORAGE_SESSION_ATTRIBUTE.equals(authStorage)) { |
| |
| this.authStorage = new SessionStorage(authName); |
| log.info("Using HTTP Session store with attribute name {}", |
| authName); |
| |
| } else { |
| |
| this.authStorage = new CookieStorage(authName); |
| log.info("Using Cookie store with name {}", authName); |
| |
| } |
| |
| this.attrCookieAuthData = OsgiUtil.toString( |
| properties.get(PAR_CREDENTIALS_ATTRIBUTE_NAME), |
| DEFAULT_CREDENTIALS_ATTRIBUTE_NAME); |
| log.info("Setting Auth Data attribute name {}", attrCookieAuthData); |
| |
| int timeoutMinutes = OsgiUtil.toInteger( |
| properties.get(PAR_AUTH_TIMEOUT), DEFAULT_AUTH_TIMEOUT); |
| if (timeoutMinutes < 1) { |
| timeoutMinutes = DEFAULT_AUTH_TIMEOUT; |
| } |
| log.info("Setting session timeout {} minutes", timeoutMinutes); |
| this.sessionTimeout = MINUTES * timeoutMinutes; |
| |
| final String tokenFileName = OsgiUtil.toString( |
| properties.get(PAR_TOKEN_FILE), DEFAULT_TOKEN_FILE); |
| final File tokenFile = getTokenFile(tokenFileName, |
| componentContext.getBundleContext()); |
| log.info("Storing tokens in {}", tokenFile.getAbsolutePath()); |
| this.tokenStore = new TokenStore(tokenFile, sessionTimeout); |
| |
| this.loginModule = null; |
| try { |
| this.loginModule = FormLoginModulePlugin.register(this, |
| componentContext.getBundleContext()); |
| } catch (Throwable t) { |
| log.info("Cannot register FormLoginModulePlugin. This is expected if Sling LoginModulePlugin services are not supported"); |
| log.debug("dump", t); |
| } |
| |
| this.includeLoginForm = OsgiUtil.toBoolean(properties.get(PAR_INCLUDE_FORM), DEFAULT_INCLUDE_FORM); |
| |
| this.loginAfterExpire = OsgiUtil.toBoolean(properties.get(PAR_LOGIN_AFTER_EXPIRE), DEFAULT_LOGIN_AFTER_EXPIRE); |
| |
| } |
| |
| protected void deactivate( |
| @SuppressWarnings("unused") ComponentContext componentContext) { |
| if (loginModule != null) { |
| loginModule.unregister(); |
| loginModule = null; |
| } |
| } |
| |
| /** |
| * Returns an absolute file indicating the file to use to persist the |
| * security tokens. |
| * <p> |
| * This method is not part of the API of this class and is package private |
| * to enable unit tests. |
| * |
| * @param tokenFileName The configured file name, must not be null |
| * @param bundleContext The BundleContext to use to make an relative file |
| * absolute |
| * @return The absolute file |
| */ |
| File getTokenFile(final String tokenFileName, |
| final BundleContext bundleContext) { |
| File tokenFile = new File(tokenFileName); |
| if (tokenFile.isAbsolute()) { |
| return tokenFile; |
| } |
| |
| tokenFile = bundleContext.getDataFile(tokenFileName); |
| if (tokenFile == null) { |
| final String slingHome = bundleContext.getProperty("sling.home"); |
| if (slingHome != null) { |
| tokenFile = new File(slingHome, tokenFileName); |
| } else { |
| tokenFile = new File(tokenFileName); |
| } |
| } |
| |
| return tokenFile.getAbsoluteFile(); |
| } |
| |
| /** |
| * Returns the user id from the authentication data. If the authentication |
| * data is a non-<code>null</code> value with 3 fields separated by an @ |
| * sign, the value of the third field is returned. Otherwise |
| * <code>null</code> is returned. |
| * <p> |
| * This method is not part of the API of this class and is package private |
| * to enable unit tests. |
| * |
| * @param authData |
| * @return |
| */ |
| String getUserId(final String authData) { |
| if (authData != null) { |
| String[] parts = StringUtils.split(authData, "@"); |
| if (parts != null && parts.length == 3) { |
| return parts[2]; |
| } |
| } |
| return null; |
| } |
| |
| /** |
| * Refresh the cookie periodically. |
| * |
| * @param sessionTimeout time to live for the session |
| * @return true or false |
| */ |
| private boolean needsRefresh(final String authData, |
| final long sessionTimeout) { |
| boolean updateCookie = false; |
| if (authData == null) { |
| updateCookie = true; |
| } else { |
| String[] parts = StringUtils.split(authData, "@"); |
| if (parts != null && parts.length == 3) { |
| long cookieTime = Long.parseLong(parts[1].substring(1)); |
| if (System.currentTimeMillis() + (sessionTimeout / 2) > cookieTime) { |
| updateCookie = true; |
| } |
| } |
| } |
| return updateCookie; |
| } |
| |
| /** |
| * The <code>AuthenticationStorage</code> interface abstracts the API |
| * required to store the {@link CookieAuthData} in an HTTP cookie or in an |
| * HTTP Session. The concrete class -- {@link CookieExtractor} or |
| * {@link SessionExtractor} -- is selected using the |
| * {@link CookieAuthenticationHandler#PAR_AUTH_HASH_STORAGE} configuration |
| * parameter, {@link CookieExtractor} by default. |
| */ |
| private static interface AuthenticationStorage { |
| String extractAuthenticationInfo(HttpServletRequest request); |
| |
| void set(HttpServletRequest request, HttpServletResponse response, |
| String authData); |
| |
| void clear(HttpServletRequest request, HttpServletResponse response); |
| } |
| |
| /** |
| * The <code>CookieExtractor</code> class supports storing the |
| * {@link CookieAuthData} in an HTTP Cookie. |
| */ |
| private static class CookieStorage implements AuthenticationStorage { |
| private final String cookieName; |
| |
| public CookieStorage(final String cookieName) { |
| this.cookieName = cookieName; |
| } |
| |
| public String extractAuthenticationInfo(HttpServletRequest request) { |
| Cookie[] cookies = request.getCookies(); |
| if (cookies != null) { |
| for (Cookie cookie : cookies) { |
| if (this.cookieName.equals(cookie.getName())) { |
| // found the cookie, so try to extract the credentials |
| // from it |
| String value = cookie.getValue(); |
| |
| // reverse the base64 encoding |
| try { |
| return new String(Base64.decodeBase64(value), |
| "UTF-8"); |
| } catch (UnsupportedEncodingException e1) { |
| throw new RuntimeException(e1); |
| } |
| } |
| } |
| } |
| |
| return null; |
| } |
| |
| public void set(HttpServletRequest request, |
| HttpServletResponse response, String authData) { |
| // base64 encode to handle any special characters |
| String cookieValue; |
| try { |
| cookieValue = Base64.encodeBase64URLSafeString(authData.getBytes("UTF-8")); |
| } catch (UnsupportedEncodingException e1) { |
| throw new RuntimeException(e1); |
| } |
| |
| // send the cookie to the response |
| setCookie(request, response, cookieValue, -1); |
| } |
| |
| public void clear(HttpServletRequest request, |
| HttpServletResponse response) { |
| Cookie oldCookie = null; |
| Cookie[] cookies = request.getCookies(); |
| if (cookies != null) { |
| for (Cookie cookie : cookies) { |
| if (this.cookieName.equals(cookie.getName())) { |
| // found the cookie |
| oldCookie = cookie; |
| break; |
| } |
| } |
| } |
| |
| // remove the old cookie from the client |
| if (oldCookie != null) { |
| setCookie(request, response, "", 0); |
| } |
| } |
| |
| private void setCookie(final HttpServletRequest request, |
| final HttpServletResponse response, final String value, |
| final int age) { |
| |
| final String ctxPath = request.getContextPath(); |
| final String cookiePath = (ctxPath == null || ctxPath.length() == 0) |
| ? "/" |
| : ctxPath; |
| |
| Cookie cookie = new Cookie(this.cookieName, value); |
| cookie.setMaxAge(age); |
| cookie.setPath(cookiePath); |
| cookie.setSecure(request.isSecure()); |
| response.addCookie(cookie); |
| } |
| } |
| |
| /** |
| * The <code>SessionExtractor</code> class provides support to store the |
| * {@link CookieAuthData} in an HTTP Session. |
| */ |
| private static class SessionStorage implements AuthenticationStorage { |
| private final String sessionAttributeName; |
| |
| SessionStorage(final String sessionAttributeName) { |
| this.sessionAttributeName = sessionAttributeName; |
| } |
| |
| public String extractAuthenticationInfo(HttpServletRequest request) { |
| HttpSession session = request.getSession(false); |
| if (session != null) { |
| Object attribute = session.getAttribute(sessionAttributeName); |
| if (attribute instanceof String) { |
| return (String) attribute; |
| } |
| } |
| return null; |
| } |
| |
| public void set(HttpServletRequest request, |
| HttpServletResponse response, String authData) { |
| // store the auth hash as a session attribute |
| HttpSession session = request.getSession(); |
| session.setAttribute(sessionAttributeName, authData); |
| } |
| |
| public void clear(HttpServletRequest request, |
| HttpServletResponse response) { |
| HttpSession session = request.getSession(false); |
| if (session != null) { |
| session.removeAttribute(sessionAttributeName); |
| } |
| } |
| |
| } |
| } |