DistributedJtiValidator.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.time.Duration;
import java.util.Date;
import org.openspcoop2.pdd.core.controllo_traffico.policy.driver.redisson.RedissonManager;
import org.openspcoop2.pdd.core.token.TokenException;
import org.openspcoop2.pdd.core.token.dpop.jti.IJtiValidator;
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.redisson.api.RBucket;
import org.redisson.api.RedissonClient;
import org.slf4j.Logger;
/**
* DistributedJtiValidator - Validatore JTI con storage Redis distribuito via Redisson
*
* Strategia:
* - Storage Redis condiviso tra tutti i nodi del cluster GovWay
* - TTL per-entry calcolato precisamente: (iat + toleranceMillis - currentTimeMillis) / 1000
* - Namespace isolato per policy: dpop:jti:{policyName}:{jti}
* - Operazione atomica SETNX via RBucket per garantire unicità distribuita
* - Valore: Boolean.TRUE (presenza = già usato)
*
* Vantaggi: Protezione replay cross-node, TTL preciso per ogni JTI
* Trade-off: Richiede Redis disponibile, latenza di rete
*
* @author Poli Andrea (apoli@link.it)
* @author $Author$
* @version $Rev$, $Date$
*/
public class DistributedJtiValidator implements IJtiValidator {
private static final Logger logger = OpenSPCoop2Logger.getLoggerOpenSPCoopCore();
private static final String KEY_PREFIX = "dpop:jti:";
/**
* Costruttore
*/
public DistributedJtiValidator() {
logger.debug("DistributedJtiValidator initialized (using shared RedissonManager)");
}
@Override
public void validateAndStore(String policyName, String jti, IDPoPParser dpopParser,
long toleranceMillis) throws TokenException, UtilsException {
// Ottiene RedissonClient dal manager condiviso
RedissonClient redisson = RedissonManager.getRedissonClient(true);
if (redisson == null) {
throw new UtilsException("RedissonClient not available - cannot validate JTI");
}
/** Costruisce chiave Redis con namespace: dpop:jti:{policyName}:{jti} */
String redisKey = KEY_PREFIX + policyName + ":" + jti;
// Ottiene RBucket per questa chiave
RBucket<Boolean> bucket = redisson.getBucket(redisKey);
// Check atomico: se esiste già -> replay attack
if (bucket.isExists()) {
logger.warn("DPoP replay attack detected for policy [{}]: JTI [{}] already used (distributed check via Redis)", policyName, jti);
throw new TokenException("DPoP JTI ["+jti+"] has already been used (replay attack detected)");
}
// Calcola TTL preciso per-entry: tempo rimanente fino alla scadenza del DPoP
Date iat = dpopParser.getIssuedAt();
long ttlSeconds = calculateTTL(iat, toleranceMillis);
if (ttlSeconds <= 0) {
// DPoP già scaduto - non ha senso memorizzarlo
logger.debug("DPoP with JTI [{}] already expired (ttl={}s) - not stored", jti, ttlSeconds);
throw new TokenException("DPoP token expired: iat ["+iat+"] + tolerance ["+toleranceMillis+"ms] exceeded");
}
// Store atomico con TTL: SETNX + EXPIRE in operazione singola
bucket.set(Boolean.TRUE, Duration.ofSeconds(ttlSeconds));
logger.debug("DPoP JTI [{}] stored successfully in Redis for policy [{}] with TTL={}s", jti, policyName, ttlSeconds);
}
/**
* Calcola TTL preciso per il JTI basato su iat e tolerance
*
* TTL = (iat + toleranceMillis - currentTimeMillis) / 1000
* Garantisce che la chiave Redis scada esattamente quando il DPoP non è più valido.
*
* @param iat Issued At timestamp del DPoP
* @param toleranceMillis Tolerance totale in milliseconds
* @return TTL in secondi, o 0 se già scaduto
*/
private long calculateTTL(Date iat, long toleranceMillis) {
if (iat == null) {
// Fallback: se iat non presente, usa tolerance come TTL
// (non dovrebbe mai accadere - iat già validato prima)
return toleranceMillis / 1000;
}
long iatMillis = iat.getTime();
long currentMillis = DateManager.getTimeMillis();
long expirationMillis = iatMillis + toleranceMillis;
long remainingMillis = expirationMillis - currentMillis;
long ttlSeconds = remainingMillis / 1000;
// Arrotonda per eccesso per evitare scadenze premature
if (remainingMillis % 1000 > 0) {
ttlSeconds++;
}
return Math.max(0, ttlSeconds);
}
@Override
public boolean isAvailable() {
// Verifica disponibilità RedissonClient
try {
return RedissonManager.isRedissonClientInitialized() &&
RedissonManager.getRedissonClient(false) != null;
} catch (Exception e) {
logger.warn("Redis not available for JTI validation: {}", e.getMessage(), e);
return false;
}
}
}