LocalJtiValidator.java
/*
* GovWay - A customizable API Gateway
* https://govway.org
*
* Copyright (c) 2005-2026 Link.it srl (https://link.it).
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3, as published by
* the Free Software Foundation.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
package org.openspcoop2.pdd.core.token.dpop.jti.impl;
import java.util.Date;
import org.openspcoop2.pdd.core.token.TokenException;
import org.openspcoop2.pdd.core.token.dpop.jti.IJtiValidator;
import org.openspcoop2.pdd.core.token.dpop.jti.JtiEntry;
import org.openspcoop2.pdd.core.token.dpop.jti.LocalJtiCacheManager;
import org.openspcoop2.pdd.core.token.parser.IDPoPParser;
import org.openspcoop2.pdd.logger.OpenSPCoop2Logger;
import org.openspcoop2.utils.UtilsException;
import org.openspcoop2.utils.date.DateManager;
import org.slf4j.Logger;
import com.github.benmanes.caffeine.cache.Cache;
/**
* LocalJtiValidator - Validatore JTI con cache in-memory Caffeine per-policy
*
* Strategia:
* - Una cache Caffeine dedicata per ogni policy (via LocalJtiCacheManager)
* - TTL per-entry: ogni JTI scade esattamente a (iat + tolerance) come Redis
* - MaxSize dimensionato per policy: (req/sec) × TTL × safety_margin
* - Chiave cache: solo JTI (no prefix policy - cache già dedicata)
* - Valore: JtiEntry (contiene iat + tolerance per calcolo TTL preciso)
*
* TTL preciso garantisce:
* - No over-retention (entry scade quando DPoP non più valido)
* - No falsi positivi replay attack (entry scadute non presenti)
* - Coerenza con DistributedJtiValidator (Redis)
*
* @author Poli Andrea (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class LocalJtiValidator implements IJtiValidator {
private static final Logger logger = OpenSPCoop2Logger.getLoggerOpenSPCoopCore();
private final long maxSize;
private final boolean failOnFull;
/**
* Costruttore
*
* @param maxSize Dimensione massima cache (da validazioneDPoPJtiMaxSize)
* @param failOnFull true per Reject Policy (blocca richieste se piena), false per LRU Policy (evict automatico)
*/
public LocalJtiValidator(long maxSize, boolean failOnFull) {
this.maxSize = maxSize;
this.failOnFull = failOnFull;
String policy = failOnFull ? "Reject Policy" : "LRU Policy";
logger.debug("LocalJtiValidator initialized with maxSize={}, policy={}", maxSize, policy);
}
@Override
public void validateAndStore(String policyName, String jti, IDPoPParser dpopParser,
long toleranceMillis) throws TokenException, UtilsException {
// Lazy initialization: ottiene o crea cache per questa policy
Cache<String, JtiEntry> cache = LocalJtiCacheManager.getOrCreateCache(policyName, this.maxSize);
// Check replay: se JTI già presente -> replay attack
JtiEntry alreadyUsed = cache.getIfPresent(jti);
if (alreadyUsed != null) {
// Replay attack detected
logger.warn("DPoP replay attack detected for policy [{}]: JTI [{}] already used", policyName, jti);
throw new TokenException("DPoP JTI ["+jti+"] has already been used (replay attack detected)");
}
// Check capacità cache SE Reject Policy attiva
if (this.failOnFull) {
long currentSize = cache.estimatedSize();
if (currentSize >= this.maxSize) {
logger.error("JTI cache full for policy [{}]: size={}/{} - rejecting JTI [{}]", policyName, currentSize, this.maxSize, jti);
throw new TokenException("DPoP JTI validation failed: local cache capacity exceeded ("+currentSize+"/"+this.maxSize+" entries). Consider increasing cache size or switching to distributed validation");
}
}
// ALTRIMENTI (LRU Policy): Caffeine fa eviction automatica, nessun check necessario
// Validazione temporale: verifica che iat + toleranceMillis non sia scaduto
// Nota: iat validation già fatta in validazioneDPoPIat(), qui ulteriore check per coerenza TTL
Date iat = dpopParser.getIssuedAt();
if (iat != null) {
long iatMillis = iat.getTime();
long currentMillis = DateManager.getTimeMillis();
long expirationMillis = iatMillis + toleranceMillis;
if (currentMillis > expirationMillis) {
// DPoP già scaduto - non ha senso memorizzarlo
logger.debug("DPoP with JTI [{}] already expired (iat={}, expiration={}, now={}) - not stored", jti, iatMillis, expirationMillis, currentMillis);
throw new TokenException("DPoP token expired: iat ["+iat+"] + tolerance ["+toleranceMillis+"ms] exceeded");
}
}
// Store JTI con entry contenente iat + tolerance per TTL preciso
JtiEntry entry = new JtiEntry(iat, toleranceMillis);
cache.put(jti, entry);
logger.debug("DPoP JTI [{}] stored successfully for policy [{}] (expires at {}ms)", jti, policyName, entry.getExpirationMillis());
}
@Override
public boolean isAvailable() {
// Cache in-memory sempre disponibile (non dipende da risorse esterne)
return true;
}
}