001/* 002 * This library is part of OpenCms - 003 * the Open Source Content Management System 004 * 005 * Copyright (c) Alkacon Software GmbH & Co. KG (http://www.alkacon.com) 006 * 007 * This library is free software; you can redistribute it and/or 008 * modify it under the terms of the GNU Lesser General Public 009 * License as published by the Free Software Foundation; either 010 * version 2.1 of the License, or (at your option) any later version. 011 * 012 * This library is distributed in the hope that it will be useful, 013 * but WITHOUT ANY WARRANTY; without even the implied warranty of 014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 015 * Lesser General Public License for more details. 016 * 017 * For further information about Alkacon Software, please see the 018 * company website: http://www.alkacon.com 019 * 020 * For further information about OpenCms, please see the 021 * project website: http://www.opencms.org 022 * 023 * You should have received a copy of the GNU Lesser General Public 024 * License along with this library; if not, write to the Free Software 025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 026 */ 027 028package org.opencms.security; 029 030import org.opencms.file.CmsObject; 031import org.opencms.file.CmsUser; 032import org.opencms.main.CmsException; 033import org.opencms.main.CmsLog; 034import org.opencms.util.CmsStringUtil; 035 036import java.io.UnsupportedEncodingException; 037import java.util.List; 038import java.util.Map; 039 040import org.apache.commons.codec.binary.Hex; 041import org.apache.commons.lang3.RandomStringUtils; 042import org.apache.commons.logging.Log; 043 044import com.google.common.collect.Lists; 045 046/** 047 * Creates and validates persisten login tokens for users.<p> 048 * 049 * When a token is created for a user, a special additional info item is stored on the user, such that 050 * the token uniquely identifies that info item. The value of the info item is the expiration date of the token. 051 * A token is validated by looking up the additional info item for the user and checking whether the token is still valid 052 * according to the stored expiration date.<p> 053 */ 054public class CmsPersistentLoginTokenHandler { 055 056 /** 057 * Bean representing the data encoded in a login token (user name and key).<p> 058 */ 059 public static class Token { 060 061 /** Separator to use for the encoded token string. */ 062 public static final String SEPARATOR = "|"; 063 064 /** The key. */ 065 private String m_key; 066 067 /** The name. */ 068 private String m_name; 069 070 /** 071 * Creates a new token object from the encoded representation.<p> 072 * 073 * @param token a string containing the token data 074 */ 075 public Token(String token) { 076 077 if (token != null) { 078 List<String> parts = CmsStringUtil.splitAsList(token, SEPARATOR); 079 if (parts.size() == 2) { 080 m_name = decodeName(parts.get(0)); 081 m_key = parts.get(1); 082 } 083 } 084 } 085 086 /** 087 * Creates a token object from the given name and key.<p> 088 * 089 * @param name the name 090 * @param key the key 091 */ 092 public Token(String name, String key) { 093 094 m_name = name; 095 m_key = key; 096 097 } 098 099 /** 100 * Gets the encoded token string representation.<p> 101 * 102 * @return the token string 103 */ 104 public String encode() { 105 106 return encodeName(m_name) + SEPARATOR + m_key; 107 } 108 109 /** 110 * Gets the additional info key to use for this token.<p> 111 * 112 * @return the additional info key 113 */ 114 public String getAdditionalInfoKey() { 115 116 return KEY_PREFIX + m_key; 117 } 118 119 /** 120 * Gets the key for this token.<p> 121 * 122 * @return the key 123 */ 124 public String getKey() { 125 126 return m_key; 127 128 } 129 130 /** 131 * Gets the user name for this token.<p> 132 * 133 * @return the user name 134 */ 135 public String getName() { 136 137 return m_name; 138 } 139 140 /** 141 * Checks if this token is valid.<p> 142 * 143 * @return true if this token is valid 144 */ 145 public boolean isValid() { 146 147 return (m_name != null) && (m_key != null); 148 } 149 150 /** 151 * Decodes the user name from a hexadecimal string, and returns null if this is not possible.<p> 152 * 153 * @param nameHex the encoded name 154 * @return the decoded name 155 */ 156 @SuppressWarnings("synthetic-access") 157 private String decodeName(String nameHex) { 158 159 try { 160 return new String(Hex.decodeHex(nameHex.toCharArray()), "UTF-8"); 161 } catch (Exception e) { 162 LOG.warn(e.getLocalizedMessage(), e); 163 return null; 164 } 165 } 166 167 /** 168 * Encodes a user name as a hex string for storing in the cookie.<p> 169 * 170 * @param name the user name 171 * 172 * @return the encoded name 173 */ 174 private String encodeName(String name) { 175 176 try { 177 return Hex.encodeHexString(name.getBytes("UTF-8")); 178 } catch (UnsupportedEncodingException e) { 179 // shouldn't happen 180 throw new IllegalStateException("UTF8 not supported"); 181 } 182 } 183 } 184 185 /** Default token lifetime. */ 186 public static final long DEFAULT_LIFETIME = 1000 * 60 * 60 * 8; 187 188 /** Prefix used for the keys for the additional infos this class creates. */ 189 public static final String KEY_PREFIX = "logintoken_"; 190 191 /** The logger for this class. */ 192 private static final Log LOG = CmsLog.getLog(CmsPersistentLoginTokenHandler.class); 193 194 /** Admin CMS context. */ 195 private static CmsObject m_adminCms; 196 197 /** The lifetime for created tokens. */ 198 private long m_lifetime = DEFAULT_LIFETIME; 199 200 /** 201 * Creates a new instance.<p> 202 */ 203 public CmsPersistentLoginTokenHandler() { 204 205 // Default constructor, do nothing 206 } 207 208 /** 209 * Static method used to give this class access to an admin cms context.<p> 210 * 211 * @param adminCms the admin cms context to set 212 */ 213 public static void setAdminCms(CmsObject adminCms) { 214 215 if (m_adminCms == null) { 216 m_adminCms = adminCms; 217 } 218 } 219 220 /** 221 * Generates a new login token for a given user and registers the token in the user's additional info.<p> 222 * 223 * @param cms the CMS context for which to create a new token 224 * @return the generated token 225 * 226 * @throws CmsException if something goes wrong 227 */ 228 public String createToken(CmsObject cms) throws CmsException { 229 230 CmsUser user = cms.getRequestContext().getCurrentUser(); 231 String key = RandomStringUtils.randomAlphanumeric(16); 232 Token tokenObj = new Token(user.getName(), key); 233 String token = tokenObj.encode(); 234 String addInfoKey = tokenObj.getAdditionalInfoKey(); 235 String value = "" + (System.currentTimeMillis() + m_lifetime); 236 user.getAdditionalInfo().put(addInfoKey, value); 237 removeExpiredTokens(user, System.currentTimeMillis()); 238 LOG.info("Generated token for user " + user.getName() + " using key " + key); 239 m_adminCms.writeUser(user); 240 241 return token; 242 } 243 244 /** 245 * Invalidates all tokens for the given user.<p> 246 * 247 * @param user the user 248 * @param token the token string 249 * 250 * @throws CmsException if something goes wrong 251 */ 252 public void invalidateToken(CmsUser user, String token) throws CmsException { 253 254 Token tokenObj = new Token(token); 255 if (tokenObj.isValid()) { 256 String addInfoKey = tokenObj.getAdditionalInfoKey(); 257 if (null != user.getAdditionalInfo().remove(addInfoKey)) { 258 m_adminCms.writeUser(user); 259 } 260 } 261 } 262 263 /** 264 * Removes expired tokens from the user's additional infos.<p> 265 * 266 * This method does not write the user back to the database. 267 * 268 * @param user the user for which to remove the additional infos 269 * @param now the current time 270 */ 271 public void removeExpiredTokens(CmsUser user, long now) { 272 273 List<String> toRemove = Lists.newArrayList(); 274 for (Map.Entry<String, Object> entry : user.getAdditionalInfo().entrySet()) { 275 String key = entry.getKey(); 276 if (key.startsWith(KEY_PREFIX)) { 277 try { 278 long expiry = Long.parseLong((String)entry.getValue()); 279 if (expiry < now) { 280 toRemove.add(key); 281 } 282 } catch (NumberFormatException e) { 283 toRemove.add(key); 284 } 285 } 286 } 287 LOG.info("Removing " + toRemove.size() + " expired tokens for user " + user.getName()); 288 for (String removeKey : toRemove) { 289 user.getAdditionalInfo().remove(removeKey); 290 } 291 } 292 293 /** 294 * Sets the token lifetime.<p> 295 * 296 * @param duration the number of milliseconds for which the token should be valid 297 */ 298 public void setTokenLifetime(long duration) { 299 300 m_lifetime = duration; 301 } 302 303 /** 304 * Validates a token and returns the matching user for which the token is valid.<p> 305 * 306 * Returns null if no user matching the token is found, or if the token for the user is expired 307 * 308 * @param tokenString the token for which to find the matching user 309 * 310 * @return the matching user for the token, or null if no matching user was found or the token is expired 311 */ 312 public CmsUser validateToken(String tokenString) { 313 314 if (CmsStringUtil.isEmpty(tokenString)) { 315 return null; 316 } 317 Token token = new Token(tokenString); 318 if (!token.isValid()) { 319 LOG.warn("Invalid token: " + tokenString); 320 return null; 321 } 322 String name = token.getName(); 323 String key = token.getKey(); 324 String logContext = "[user=" + name + ",key=" + key + "] "; 325 try { 326 CmsUser user = m_adminCms.readUser(name); 327 String infoKey = token.getAdditionalInfoKey(); 328 String addInfoValue = (String)user.getAdditionalInfo().get(infoKey); 329 logContext = logContext + "[value=" + addInfoValue + "]"; 330 if (addInfoValue == null) { 331 LOG.warn(logContext + " no matching additional info value found"); 332 return null; 333 } 334 try { 335 long expirationDate = Long.parseLong(addInfoValue); 336 if (System.currentTimeMillis() > expirationDate) { 337 LOG.warn(logContext + "Login token expired"); 338 user.getAdditionalInfo().remove(infoKey); 339 try { 340 m_adminCms.writeUser(user); 341 } catch (Exception e) { 342 LOG.error(e.getLocalizedMessage(), e); 343 } 344 return null; 345 } 346 } catch (NumberFormatException e) { 347 LOG.warn(logContext + "Invalid format for login token additional info"); 348 return null; 349 } 350 return user; 351 } catch (Exception e) { 352 LOG.warn(logContext + "error validating token", e); 353 return null; 354 } 355 } 356}