L’autenticazione a due fattori (2FA) aggiunge un secondo step oltre a username e password, riducendo in modo drastico il rischio di compromissione dell’account. In questa guida implementiamo la variante più comune: i codici TOTP (Time-based One-Time Password) compatibili con app come Google Authenticator, 1Password, Authy e Microsoft Authenticator.
Come funziona il TOTP (RFC 6238) in 60 secondi
- Il server genera e memorizza un secret condiviso (Base32) per l’utente.
- Il segreto viene “provisionato” sul dispositivo dell’utente tramite URI
otpauth://o QR code. - Ad ogni login, l’utente inserisce il codice TOTP (6–8 cifre) che l’app calcola come HMAC del tempo attuale a step (di solito 30s) con il secret.
- Il server ricalcola localmente il TOTP e verifica la corrispondenza (con una piccola finestra di tolleranza).
Dipendenze consigliate
Useremo librerie mature e leggere per Base32 e QR code. Con Maven:
<dependencies>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.16.0</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>core</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>3.5.3</version>
</dependency>
</dependencies>
1) Generare il secret utente
Il segreto deve essere casuale, sufficientemente lungo (almeno 20 byte) e codificato in Base32 per compatibilità con gli authenticator.
import java.security.SecureRandom;
import org.apache.commons.codec.binary.Base32;
public final class TotpSecrets {
private static final SecureRandom RNG = new SecureRandom();
public static String newBase32Secret(int numBytes) {
byte[] buf = new byte[numBytes];
RNG.nextBytes(buf);
Base32 base32 = new Base32();
return base32.encodeToString(buf).replace("=", ""); // padding opzionale
}
// Comodo default: 20 byte ~ 160 bit
public static String newBase32Secret() {
return newBase32Secret(20);
}
}
2) Creare l’URI otpauth e il QR code
Formato standard: otpauth://totp/<issuer>:<accountName>?secret=<BASE32>&issuer=<issuer>&period=30&digits=6&algorithm=SHA1
import java.io.ByteArrayOutputStream;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.EnumMap;
import com.google.zxing.*;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
public final class TotpProvisioning {
public static String buildOtpAuthUri(
String issuer, String accountName, String base32Secret,
int digits, int period, String algorithm) {
String label = url(issuer) + ":" + url(accountName);
String params = "secret=" + base32Secret
+ "&issuer=" + url(issuer)
+ "&period=" + period
+ "&digits=" + digits
+ "&algorithm=" + url(algorithm);
return "otpauth://totp/" + label + "?" + params;
}
public static byte[] qrPng(String text, int size) {
try {
EnumMap<EncodeHintType, Object> hints = new EnumMap<>(EncodeHintType.class);
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
hints.put(EncodeHintType.MARGIN, 1);
BitMatrix matrix = new MultiFormatWriter()
.encode(text, BarcodeFormat.QR_CODE, size, size, hints);
ByteArrayOutputStream out = new ByteArrayOutputStream();
MatrixToImageWriter.writeToStream(matrix, "PNG", out);
return out.toByteArray(); // salvare/servire come image/png
} catch (Exception e) {
throw new RuntimeException("Impossibile generare il QR", e);
}
}
private static String url(String s) {
return URLEncoder.encode(s, StandardCharsets.UTF_8);
}
}
3) Calcolare e verificare il TOTP lato server
Implementiamo il TOTP (RFC 6238) con HMAC-SHA1/256/512. Per compatibilità massima usare SHA1, 6 cifre, periodo 30s.
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.codec.binary.Base32;
import java.nio.ByteBuffer;
import java.time.Instant;
import java.util.concurrent.TimeUnit;
public final class Totp {
public static int generate(String base32Secret, long timestampSeconds,
int digits, int periodSeconds, String algorithm) {
byte[] key = new Base32().decode(base32Secret);
long counter = timestampSeconds / periodSeconds;
byte[] msg = ByteBuffer.allocate(8).putLong(counter).array();
try {
Mac mac = Mac.getInstance("Hmac" + algorithm);
mac.init(new SecretKeySpec(key, "Hmac" + algorithm));
byte[] hash = mac.doFinal(msg);
int offset = hash[hash.length - 1] & 0x0F;
int binary = ((hash[offset] & 0x7F) << 24)
| ((hash[offset + 1] & 0xFF) << 16)
| ((hash[offset + 2] & 0xFF) << 8)
| (hash[offset + 3] & 0xFF);
int otp = binary % (int) Math.pow(10, digits);
return otp;
} catch (Exception e) {
throw new RuntimeException("Errore TOTP", e);
}
}
public static boolean verify(String base32Secret, String code,
int digits, int periodSeconds, String algorithm,
int window) {
long now = Instant.now().getEpochSecond();
for (int i = -window; i <= window; i++) {
long ts = now + (long) i * periodSeconds;
int candidate = generate(base32Secret, ts, digits, periodSeconds, algorithm);
if (padded(candidate, digits).equals(code)) return true;
}
return false;
}
private static String padded(int n, int digits) {
String s = Integer.toString(n);
return "0".repeat(Math.max(0, digits - s.length())) + s;
}
public static String currentCode(String base32Secret) {
int code = generate(base32Secret, Instant.now().getEpochSecond(), 6, 30, "SHA1");
return padded(code, 6);
}
}
4) Flusso di onboarding e login
- Onboarding 2FA: dopo l’autenticazione con password, genera il segreto, salva nel database e mostra QR/URI. Richiedi all’utente di inserire un primo codice per confermare.
- Login: verifica le credenziali, poi richiedi il codice TOTP e validalo con una finestra temporale (per esempio ±1 step).
Esempio di servizio per l’onboarding
public class TwoFaService {
public record Provisioning(String secretBase32, String otpAuthUri, byte\[] qrPng) {}
public Provisioning enable2FA(String userId, String issuer, String accountName) {
String secret = TotpSecrets.newBase32Secret();
String uri = TotpProvisioning.buildOtpAuthUri(issuer, accountName, secret, 6, 30, "SHA1");
byte\[] qr = TotpProvisioning.qrPng(uri, 256);
// TODO: persist "secret" in modo sicuro associato a userId
return new Provisioning(secret, uri, qr);
}
public boolean verify(String userId, String code) {
// TODO: carica "secret" dal DB
String secret = loadSecretFor(userId);
return Totp.verify(secret, code, 6, 30, "SHA1", 1);
}
private String loadSecretFor(String userId) { throw new UnsupportedOperationException(); }
}
5) Integrazione con Spring Security (opzionale)
Schema comune: primo step con username/password ottenendo uno stato “parzialmente autenticato”; secondo step che verifica il TOTP e promuove l’utente a completamente autenticato.
import org.springframework.security.authentication.*;
import org.springframework.security.core.*;
import org.springframework.security.web.authentication.*;
public class TotpAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public TotpAuthenticationFilter() {
super("/login/totp");
setAuthenticationSuccessHandler(new SavedRequestAwareAuthenticationSuccessHandler());
}
@Override
public Authentication attemptAuthentication(javax.servlet.http.HttpServletRequest req,
javax.servlet.http.HttpServletResponse res) {
String userId = req.getParameter("userId");
String code = req.getParameter("code");
boolean ok = twoFaService.verify(userId, code);
if (!ok) throw new BadCredentialsException("Invalid TOTP");
return new UsernamePasswordAuthenticationToken(userId, null, List.of());
}
private final TwoFaService twoFaService = new TwoFaService();
}
Persistenza e sicurezza del secret
- Crittografa i secret a riposo: usa una chiave KMS o una master key protetta da HSM.
- Proteggi in memoria: azzera i buffer dove possibile e limita i log.
- Rate limiting sulla verifica TOTP per utente/IP.
- Recovery codes: genera una manciata di codici statici monouso stampabili dall’utente.
- Disallineamenti orari: usa NTP sul server; imposta una finestra di verifica (±1 o ±2 passi).
- Revoca: consenti di disattivare/rigenerare il secret dopo una verifica forte.
Test rapidi
# Esegui un main di prova per stampare un codice corrente
jshell --class-path target/dependency/*:target/classes <<'EOF'
import TotpSecrets, Totp;
var secret = TotpSecrets.newBase32Secret();
System.out.println("Secret: " + secret);
System.out.println("Code: " + Totp.currentCode(secret));
EOF
Domande frequenti
Posso usare SHA256/512? Sì: aggiorna algorithm coerentemente su server e nel parametro dell’URI (molte app lo supportano).
Posso validare codici di 8 cifre? Sì: imposta digits=8 su generazione e verifica, e nell’URI.
Checklist di produzione
- Onboarding con conferma del primo codice.
- Rate limit e audit logging (senza mai loggare i secret).
- Recovery codes e flusso di reset sicuro.
- Backup/replica dei secret crittografati.
- Monitoraggio degli errori e drift dell’orologio.
Conclusione
Con poche classi e qualche accortezza di sicurezza, puoi offrire una 2FA TOTP interoperabile con le principali app di autenticazione. Gli esempi mostrati sono pensati per essere integrati facilmente in qualsiasi applicazione Java o Spring Boot.