/**
* Copyright (C) 2009 eXo Platform SAS.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.exoplatform.web.security.security;
import java.security.SecureRandom;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import org.exoplatform.container.PortalContainer;
import org.exoplatform.container.xml.InitParams;
import org.exoplatform.container.xml.ValueParam;
import org.exoplatform.management.annotations.Impact;
import org.exoplatform.management.annotations.ImpactType;
import org.exoplatform.management.annotations.Managed;
import org.exoplatform.management.annotations.ManagedDescription;
import org.exoplatform.management.jmx.annotations.NameTemplate;
import org.exoplatform.management.jmx.annotations.Property;
import org.exoplatform.web.login.LoginServlet;
import org.exoplatform.web.security.Token;
import org.exoplatform.web.security.TokenStore;
import org.gatein.common.logging.Logger;
import org.gatein.common.logging.LoggerFactory;
import org.gatein.common.util.Base64;
import org.gatein.common.util.Base64.EncodingOption;
import org.gatein.wci.security.Credentials;
import org.picocontainer.Startable;
/**
* Created by The eXo Platform SAS Author : liem.nguyen ncliam@gmail.com Jun 5, 2009
*
* todo julien : - make delay configuration from init param and @Managed setter - start/stop expiration daemon - manually invoke
* the daemon via @Managed
*
* @param <T> the token type
* @param <K> the token key type
*/
@Managed
@ManagedDescription("Token Store Service")
@NameTemplate({ @Property(key = "service", value = "TokenStore"), @Property(key = "name", value = "{Name}") })
public abstract class AbstractTokenService<T extends Token, K> implements Startable, TokenStore {
protected final Logger log = LoggerFactory.getLogger(getClass());
protected static final String SERVICE_CONFIG = "service.configuration";
protected static final String CLEANUP_PERIOD_TIME = "cleanup.period.time";
/**
* See {@link #tokenByteLength}. 8 bytes (64 bits) would be enough, but we want to get padding-less Byte64 representation,
* so we take the next greater number divisible by 3 which is 9. 9 bytes is equal to 72 bits.
*/
public static final int DEFAULT_TOKEN_BYTE_LENGTH = 9;
/**
* The number of random bits generared by {@link #nextRandom()}. Use values divisible by 3 to produce random strings
* consisting only of {@code 0-9}, {@code a-z}, {@code A-Z}, {@code -} and {@code _}, i.e. URL safe Byte64 without padding.
* If a value not divisible by 3 is used the random strings will contain {@code *} in addition to the named characters.
*/
protected final int tokenByteLength;
protected String name;
protected long validityMillis;
protected int delay_time = 600;
private ScheduledExecutorService executor;
@SuppressWarnings("unchecked")
public AbstractTokenService(InitParams initParams) throws TokenServiceInitializationException {
List<String> params = initParams.getValuesParam(SERVICE_CONFIG).getValues();
this.name = params.get(0);
long configValue = new Long(params.get(1));
this.validityMillis = TimeoutEnum.valueOf(params.get(2)).toMilisecond(configValue);
this.tokenByteLength = DEFAULT_TOKEN_BYTE_LENGTH;
ValueParam delayParam = initParams.getValueParam(CLEANUP_PERIOD_TIME);
if(delayParam != null) {
delay_time = Integer.parseInt(delayParam.getValue());
}
}
public void start() {
if(delay_time > 0) {
// start a thread, garbage expired cookie token every [DELAY_TIME]
executor = Executors.newSingleThreadScheduledExecutor();
executor.scheduleWithFixedDelay(new Runnable() {
public void run() {
try {
AbstractTokenService.this.cleanExpiredTokens();
} catch (Throwable t) {
log.warn("Failed to clean expired tokens", t);
}
}
}, 0, delay_time, TimeUnit.SECONDS);
}
}
public void stop() {
if(executor != null) {
executor.shutdown();
}
}
public static <T extends AbstractTokenService<?, ?>> T getInstance(Class<T> classType) {
PortalContainer container = PortalContainer.getInstance();
return classType.cast(container.getComponentInstanceOfType(classType));
}
public Credentials validateToken(String stringKey, boolean remove) {
if (stringKey == null) {
throw new NullPointerException();
}
//
K tokenKey = decodeKey(stringKey);
T token;
try {
if (remove) {
token = this.deleteToken(tokenKey);
} else {
token = this.getToken(tokenKey);
}
if (token != null) {
boolean valid = token.getExpirationTimeMillis() > System.currentTimeMillis();
if (valid) {
return token.getPayload();
} else if (!remove) {
this.deleteToken(tokenKey);
}
}
} catch (Exception e) {
}
return null;
}
@Managed
@ManagedDescription("Clean all tokens are expired")
@Impact(ImpactType.IDEMPOTENT_WRITE)
public abstract void cleanExpiredTokens();
@Managed
@ManagedDescription("Get time for token expiration in seconds")
public long getValidityTime() {
return validityMillis / 1000;
}
@Managed
@ManagedDescription("The expiration daemon period time in seconds")
public long getPeriodTime() {
return delay_time;
}
@Managed
@ManagedDescription("The token service name")
public String getName() {
return name;
}
public abstract T getToken(K id);
public abstract T deleteToken(K id);
/**
* Decode a key from its string representation.
*
* @param stringKey the key a s a string
* @return the typed key
*/
protected abstract K decodeKey(String stringKey);
// We don't make it a property as retrieving the value can be an expensive operation
@Managed
@ManagedDescription("The number of tokens")
@Impact(ImpactType.READ)
public abstract long size();
private enum TimeoutEnum {
SECOND(1000), MINUTE(1000 * 60), HOUR(1000 * 60 * 60), DAY(1000 * 60 * 60 * 24);
private long multiply;
private TimeoutEnum(long multiply) {
this.multiply = multiply;
}
public long toMilisecond(long configValue) {
return configValue * multiply;
}
}
protected String nextTokenId() {
return LoginServlet.COOKIE_NAME + nextRandom();
}
protected String nextRandom() {
byte[] randomBytes = new byte[tokenByteLength];
PortalContainer container = PortalContainer.getInstance();
SecureRandom random = ((SecureRandomService) container.getComponentInstanceOfType(SecureRandomService.class)).getSecureRandom();
random.nextBytes(randomBytes);
return Base64.encodeBytes(randomBytes, EncodingOption.USEURLSAFEENCODING);
}
}