Java tutorial
/* * Copyright 2012 david gonzalez. * * Licensed 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 com.activecq.tools.auth.impl; import com.activecq.api.utils.CookieUtil; import com.activecq.api.utils.OsgiPropertyUtil; import com.activecq.tools.auth.SessionAuthenticationService; import java.io.UnsupportedEncodingException; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.AbstractMap; import java.util.Date; import java.util.Dictionary; import java.util.logging.Level; import java.util.logging.Logger; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.jcr.SimpleCredentials; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang.StringUtils; import org.apache.felix.scr.annotations.*; import org.apache.sling.auth.core.spi.AuthenticationInfo; import org.apache.sling.commons.osgi.PropertiesUtil; import org.osgi.service.component.ComponentContext; @Component(label = "ActiveCQ - HTTP Cookie Authenticator", configurationFactory = true, immediate = true, metatype = true) @Properties({ @Property(label = "Service Unique ID", description = "Must be unique between other Configurations. Must be the same between all servers", name = "service.uid", value = "default"), @Property(label = "Service Ranking", description = "Service ranking. Higher gives more priority.", name = "service.ranking", intValue = 20, propertyPrivate = false), @Property(label = "Vendor", name = "service.vendor", value = "ActiveCQ", propertyPrivate = true) }) @Service public class CookieAuthenticationImpl implements SessionAuthenticationService { private static final String DATA_DELIMITER = "@"; /** * Sling Property - Cookie Name */ private String cookieName = DEFAULT_COOKIE_NAME; private static final String DEFAULT_COOKIE_NAME = "auth"; @Property(label = "Cookie Name", description = "Must be the same between all servers", value = DEFAULT_COOKIE_NAME) private static final String PROP_COOKIE_NAME = "prop.token.name"; /** * Sling Property - Cookie Secret */ private static final String DEFAULT_COOKIE_SECRET = "CHANGE_ME_BUT_KEEP_ME_THE_SAME_BETWEEN_SERVERS"; private String secret = DEFAULT_COOKIE_SECRET; @Property(label = "Cookie Secret", description = "Must be the same between all servers. A UUID is a good secret. Changing this value will invalidate previously issued Auth Cookies.", value = DEFAULT_COOKIE_SECRET) private static final String PROP_COOKIE_SECRET = "prop.token.secret"; /** * Sling Property - Cookie Life */ private static final int DEFAULT_COOKIE_EXPIRY = -1; // Session cookie private int cookieExpiry = DEFAULT_COOKIE_EXPIRY; @Property(label = "Default Cookie Life", description = "Default cookie expiry in seconds, used if 'Remember Me' expirty isn't set. Defaults to -1 to indicate a Session cookie.", intValue = DEFAULT_COOKIE_EXPIRY) private static final String PROP_COOKIE_EXPIRY = "prop.session.expiry"; private static final String DEFAULT_REMEMBER_ME = "remember-me:on"; private AbstractMap.SimpleEntry rememberMe = new AbstractMap.SimpleEntry(null, null); @Property(label = "Remember Me", description = "Request Parameter key:value pair to watch for.", value = DEFAULT_REMEMBER_ME) private static String PROP_REMEMBER_ME = "prop.session.remember-me"; /** * Sling Property - Remember Me Cookie Life */ private static final long DEFAULT_REMEMBER_ME_COOKIE_EXPIRY = 30; // 30 days private int cookieRememberMeExpiry = new Long(DEFAULT_REMEMBER_ME_COOKIE_EXPIRY).intValue(); @Property(label = "Remember Me Cookie Life", description = "Cookie expiry in days", longValue = DEFAULT_COOKIE_EXPIRY) private static final String PROP_COOKIE_REMEMBER_ME_EXPIRY = "prop.session.remember-me.expiry"; /** * Sling Property - Hash Algorithm * * http://docs.oracle.com/javase/6/docs/technotes/guides/security/StandardNames.html */ private static final String DEFAULT_ENCRYPTION_TYPE = "HmacSHA1"; private static String encryptionType = DEFAULT_ENCRYPTION_TYPE; @Property(label = "Encryption Algorithm", description = "Encryption algorithm used to hash secure cookie. Must be the same between all servers", value = DEFAULT_ENCRYPTION_TYPE, options = { @PropertyOption(name = "HMAC-SHA1", value = "HmacSHA1"), @PropertyOption(name = "HMAC-MD5", value = "HmacMD5") }) private static final String PROP_ENCRYPTION_TYPE = "prop.encryption.algorithm"; /** * Sling Property - Cookie Path */ private static final String DEFAULT_COOKIE_PATH = "/"; private String cookiePath = DEFAULT_COOKIE_PATH; @Property(label = "Cookie Path", description = "Cookie Path", value = DEFAULT_COOKIE_PATH) private static String PROP_COOKIE_PATH = "prop.cookie.path"; /** * Sling Property - Drop Cookies Regexes */ private static final String[] DEFAULT_DROP_COOKIES = new String[] {}; private String[] dropCookieRegexes = DEFAULT_DROP_COOKIES; @Property(label = "Drop Cookies", description = "Drop Cookies", cardinality = 100000) private static String PROP_DROP_COOKIES = "prop.cookie.drop-regexes"; /** * Sling Property - Cookie Encoding */ private static final String DEFAULT_COOKIE_ENCODING = "UTF-8"; private String cookieEncoding = DEFAULT_COOKIE_ENCODING; @Property(label = "Cookie Encoding", description = "Cookie character encoding (Default: UTF-8)", value = DEFAULT_COOKIE_ENCODING) private static String PROP_COOKIE_ENCODING = "prop.cookie.encoding"; /** * Determines whether this authentication service should authenticate the request * * @param request * @return */ @Override public boolean accepts(HttpServletRequest request) { return (CookieUtil.getCookie(request, cookieName) != null); } /** * Validate the Authentication Cookie * * @param request * @param cookieName * @param secret * @return */ @Override public SimpleCredentials extractCredentials(HttpServletRequest request) { Cookie cookie = CookieUtil.getCookie(request, cookieName); if (cookie == null) { return null; } // Get and decode cookie data String cookieData; try { if (StringUtils.isBlank(cookie.getValue())) { return null; } final String tmp = new Base64(true).decode(cookie.getValue()).toString(); cookieData = URLDecoder.decode(tmp, cookieEncoding); } catch (UnsupportedEncodingException e) { return null; } // Split the cookie data by the DATA_DELIMITER String[] values = splitCookieData(cookieData); if (values == null) { return null; } final String token = StringUtils.trimToNull(values[0]); final String timestamp = StringUtils.trimToNull(values[1]); final String userId = StringUtils.trimToNull(values[2]); // Could not get a required value from the cookie if (userId == null || token == null || timestamp == null) { return null; } final String expectedData; try { expectedData = encryptData(createDataToEncrypt(userId, timestamp)); } catch (NoSuchAlgorithmException ex) { Logger.getLogger(CookieAuthenticationImpl.class.getName()).log(Level.SEVERE, null, ex); return null; } catch (InvalidKeyException ex) { Logger.getLogger(CookieAuthenticationImpl.class.getName()).log(Level.SEVERE, null, ex); return null; } // If Cookie token and Expected token don't match, return null if (!StringUtils.equals(token, expectedData)) { return null; } // TODO: Handle cookie timestamping more appropriately. // Check if the current time is greater than the acceptable cookie // expiry timestamp // long cookieTimestamp = Long.parseLong(timestamp); // if (System.currentTimeMillis() > cookieTimestamp) { // return null; // } return new SimpleCredentials(userId, "".toCharArray()); } /** * * @param request * @param response * @param cookieName * @return */ @Override public boolean dropCredentials(HttpServletRequest request, HttpServletResponse response) { CookieUtil.dropCookies(request, response, cookieName); if (dropCookieRegexes != null && dropCookieRegexes.length > 0) { CookieUtil.dropCookiesByRegexArray(request, response, dropCookieRegexes); } return true; } @Override public boolean useAuthenticationFailed() { return false; } @Override public void authenticationFailed(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) { throw new UnsupportedOperationException( "CookieAuthenticationImpl.useAuthenticationFailed() returns false."); } @Override public boolean useAuthenticationSucceeded() { return false; } @Override public boolean authenticationSucceeded(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) { throw new UnsupportedOperationException( "CookieAuthenticationImpl.useAuthenticationSucceeded() returns false."); } /** * Add data to response to authenticate future session (usually a Cookie) * * @param request * @param response * @param authInfo * @return */ @Override public boolean addCredentials(HttpServletRequest request, HttpServletResponse response, AuthenticationInfo authInfo) { int expiry = cookieExpiry; final String rememberMeKey = (String) rememberMe.getKey(); boolean hasRememberMe = (StringUtils.isNotBlank(rememberMeKey)) && (StringUtils.isNotBlank(request.getParameter(rememberMeKey))); if (hasRememberMe) { String expiryValue = request.getParameter(rememberMeKey); if (StringUtils.equals(expiryValue, (String) rememberMe.getValue())) { expiry = cookieRememberMeExpiry; } } final Cookie cookie = createSessionAuthenticationCookie(authInfo.getUser(), cookiePath, expiry); CookieUtil.addCookie(cookie, response); return true; } /** * * @param userId * @param cookiePath * @param expiry * @return */ private Cookie createSessionAuthenticationCookie(String userId, String cookiePath, int expiry) { try { long expiriesAt = new Date().getTime(); if (expiry > 0) { expiriesAt += (expiry * 1000); // seconds to millseconds } final String timestamp = String.valueOf(expiriesAt); String cookieData = createCookieData(userId, timestamp); Cookie cookie = new Cookie(cookieName, cookieData); cookie.setPath(cookiePath); cookie.setMaxAge(expiry); // in seconds return cookie; } catch (UnsupportedEncodingException ex) { Logger.getLogger(CookieAuthenticationImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (NoSuchAlgorithmException ex) { Logger.getLogger(CookieAuthenticationImpl.class.getName()).log(Level.SEVERE, null, ex); } catch (InvalidKeyException ex) { Logger.getLogger(CookieAuthenticationImpl.class.getName()).log(Level.SEVERE, null, ex); } return null; } /** * * @param userId * @param token * @param timestamp * @return * @throws UnsupportedEncodingException */ private String createCookieData(String userId, String timestamp) throws UnsupportedEncodingException, NoSuchAlgorithmException, InvalidKeyException { /* HmacSHA1(<secret>,<expirytime>@<userID>)@<expirytime>@<userID> */ final String data = createPlainTextData(userId, timestamp); final String encyptedData = encryptData(createDataToEncrypt(userId, timestamp)); String tmp = encyptedData + DATA_DELIMITER + data; tmp = new Base64(true).encodeToString(tmp.getBytes()).toString(); return URLEncoder.encode(tmp, cookieEncoding); } private String createPlainTextData(String userId, String timestamp) { return timestamp + DATA_DELIMITER + userId; } private String createDataToEncrypt(String userId, String timestamp) { return secret + "," + createPlainTextData(userId, timestamp); } /** * Encrypt token data * * @param data * @return * @throws NoSuchAlgorithmException * @throws InvalidKeyException */ private String encryptData(String data) throws NoSuchAlgorithmException, InvalidKeyException { SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(), encryptionType); Mac mac = Mac.getInstance(encryptionType); mac.init(keySpec); byte[] result = mac.doFinal(data.getBytes()); return StringUtils.trim(new Base64(true).encodeToString(result)); } private String[] splitCookieData(String data) { if (StringUtils.isBlank(data)) { return null; } return StringUtils.split(data, DATA_DELIMITER); /* String[] split = new String[3]; int i = data.indexOf(DATA_DELIMITER); // No Delimiter exists anywhere in the string if(i < 0) { return null; } split[0] = data.substring(0, i); // Delimiter is the last character in the string, so return if((i + 1) >= data.length()) { return null; } data = data.substring(i + 1, data.length()); i = data.indexOf(DATA_DELIMITER); // No Delimiters exist in the plain text portion of the data if(i < 0) { return split; } split[1] = data.substring(0, i); // Delimiter is the last character in the plain text portion of the data if((i + 1) >= data.length()) { return null; } split[2] = data.substring(i + 1, data.length()); return split; */ } @SuppressWarnings("unchecked") protected void activate(ComponentContext componentContext) { Dictionary properties = componentContext.getProperties(); encryptionType = PropertiesUtil.toString(properties.get(PROP_ENCRYPTION_TYPE), DEFAULT_ENCRYPTION_TYPE); cookieName = PropertiesUtil.toString(properties.get(PROP_COOKIE_NAME), DEFAULT_COOKIE_NAME); cookiePath = PropertiesUtil.toString(properties.get(PROP_COOKIE_PATH), DEFAULT_COOKIE_PATH); cookieEncoding = PropertiesUtil.toString(properties.get(PROP_COOKIE_ENCODING), DEFAULT_COOKIE_ENCODING); secret = PropertiesUtil.toString(properties.get(PROP_COOKIE_SECRET), DEFAULT_COOKIE_SECRET); cookieExpiry = PropertiesUtil.toInteger(properties.get(PROP_COOKIE_EXPIRY), DEFAULT_COOKIE_EXPIRY); final long cookieRememberMeExpiryL = PropertiesUtil.toLong(properties.get(PROP_COOKIE_REMEMBER_ME_EXPIRY), DEFAULT_REMEMBER_ME_COOKIE_EXPIRY); cookieRememberMeExpiry = (int) (cookieRememberMeExpiryL * 86400); // Seconds in a day rememberMe = OsgiPropertyUtil .toSimpleEntry(PropertiesUtil.toString(properties.get(PROP_REMEMBER_ME), "remember-me"), ":"); } protected void deactivate(ComponentContext componentContext) { } }