From 28fdfbbaf73e83698c84a94400fbb2a0076ad152 Mon Sep 17 00:00:00 2001
From: Ny Hasina Vagno
Date: Mon, 22 Dec 2025 00:28:11 +0400
Subject: [PATCH 1/4] chore: qrcode generator deps
---
build.gradle | 3 +++
1 file changed, 3 insertions(+)
diff --git a/build.gradle b/build.gradle
index fc174ab..8ae4712 100644
--- a/build.gradle
+++ b/build.gradle
@@ -85,6 +85,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
+ implementation 'commons-codec:commons-codec:1.16.0'
+ implementation 'com.google.zxing:core:3.5.2'
+ implementation 'com.google.zxing:javase:3.5.2'
implementation 'app.esiroi:auth-gen:1.0.0'
implementation 'org.flywaydb:flyway-core'
compileOnly 'org.projectlombok:lombok'
From e8c3e379f8d5cf7426f3426eab66cf83d455b33b Mon Sep 17 00:00:00 2001
From: Ny Hasina Vagno
Date: Mon, 22 Dec 2025 00:57:12 +0400
Subject: [PATCH 2/4] feat: generate qrcode for totp
---
.../controller/AuthenticationController.java | 15 +++++---
.../auth/endpoint/mapper/UserRestMapper.java | 12 ++++++-
.../auth/endpoint/security/SecurityConf.java | 2 ++
.../auth/endpoint/security/TOTPConf.java | 12 +++----
.../{UserService.java => AuthService.java} | 26 +++++++++++++-
src/main/resources/templates/profile.html | 2 +-
src/main/resources/templates/qrcode.html | 34 +++++++++++++++++++
...{UserServiceIT.java => AuthServiceIT.java} | 6 ++--
8 files changed, 92 insertions(+), 17 deletions(-)
rename src/main/java/app/esiroi/auth/service/{UserService.java => AuthService.java} (63%)
create mode 100644 src/main/resources/templates/qrcode.html
rename src/test/java/app/esiroi/auth/integration/service/{UserServiceIT.java => AuthServiceIT.java} (87%)
diff --git a/src/main/java/app/esiroi/auth/endpoint/controller/AuthenticationController.java b/src/main/java/app/esiroi/auth/endpoint/controller/AuthenticationController.java
index 0a21a87..9c362fe 100644
--- a/src/main/java/app/esiroi/auth/endpoint/controller/AuthenticationController.java
+++ b/src/main/java/app/esiroi/auth/endpoint/controller/AuthenticationController.java
@@ -3,7 +3,7 @@
import app.esiroi.auth.endpoint.mapper.UserRestMapper;
import app.esiroi.auth.endpoint.rest.model.AuthUser;
import app.esiroi.auth.endpoint.security.AuthProvider;
-import app.esiroi.auth.service.UserService;
+import app.esiroi.auth.service.AuthService;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import lombok.AllArgsConstructor;
@@ -14,7 +14,7 @@
@Controller
@AllArgsConstructor
public class AuthenticationController {
- private final UserService service;
+ private final AuthService service;
private final UserRestMapper mapper;
@GetMapping("/")
@@ -42,8 +42,8 @@ public String logout(HttpServletResponse response) {
@PostMapping("/register")
public String register(@ModelAttribute AuthUser toRegister) {
var toSave = mapper.toDomain(toRegister);
- service.saveUser(toSave);
- return "redirect:/validateOTP";
+ var email = service.saveUser(toSave).getEmail();
+ return "redirect:/qrcode?email=" + email;
}
@GetMapping("/register")
@@ -57,6 +57,13 @@ public String validateOTP() {
return "otp";
}
+ @GetMapping("/qrcode")
+ public String qrcode(@RequestParam("email") String email, Model model) {
+ var qrCode = service.setupTotp(email);
+ model.addAttribute("qrCode", qrCode);
+ return "qrcode";
+ }
+
@GetMapping("/profile")
public String profile(Model model) {
var email = AuthProvider.getAuthenticatedUserEmail();
diff --git a/src/main/java/app/esiroi/auth/endpoint/mapper/UserRestMapper.java b/src/main/java/app/esiroi/auth/endpoint/mapper/UserRestMapper.java
index bbf4e8e..e188576 100644
--- a/src/main/java/app/esiroi/auth/endpoint/mapper/UserRestMapper.java
+++ b/src/main/java/app/esiroi/auth/endpoint/mapper/UserRestMapper.java
@@ -5,7 +5,9 @@
import app.esiroi.auth.endpoint.rest.model.AuthUser;
import app.esiroi.auth.endpoint.rest.model.User;
import app.esiroi.auth.endpoint.security.Encryptor;
+import java.security.SecureRandom;
import lombok.AllArgsConstructor;
+import org.apache.commons.codec.binary.Base32;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@@ -26,7 +28,7 @@ public User toRest(app.esiroi.auth.model.User user) {
public app.esiroi.auth.model.User toDomain(AuthUser user) {
var hashedPass = passwordEncoder.encode(user.getPassword());
- var encryptedSecret = encryptor.getInstance().encrypt("test".getBytes());
+ var encryptedSecret = encryptor.getInstance().encrypt(generateSecret().getBytes());
return app.esiroi.auth.model.User.builder()
.id(randomUUID().toString())
.email(user.getEmail())
@@ -34,4 +36,12 @@ public app.esiroi.auth.model.User toDomain(AuthUser user) {
.otpSecret(encryptedSecret)
.build();
}
+
+ private String generateSecret() {
+ SecureRandom random = new SecureRandom();
+ byte[] bytes = new byte[20]; // 160 bits
+ random.nextBytes(bytes);
+ Base32 base32 = new Base32();
+ return base32.encodeToString(bytes);
+ }
}
diff --git a/src/main/java/app/esiroi/auth/endpoint/security/SecurityConf.java b/src/main/java/app/esiroi/auth/endpoint/security/SecurityConf.java
index 2ce39ed..ae7ec71 100644
--- a/src/main/java/app/esiroi/auth/endpoint/security/SecurityConf.java
+++ b/src/main/java/app/esiroi/auth/endpoint/security/SecurityConf.java
@@ -70,6 +70,8 @@ public SecurityFilterChain configure(HttpSecurity http) throws Exception {
.authenticated()
.requestMatchers(POST, "/validateOTP")
.authenticated()
+ .requestMatchers(GET, "/qrcode")
+ .permitAll()
.requestMatchers(GET, "/profile")
.authenticated()
.anyRequest()
diff --git a/src/main/java/app/esiroi/auth/endpoint/security/TOTPConf.java b/src/main/java/app/esiroi/auth/endpoint/security/TOTPConf.java
index 26edce7..f9c433b 100644
--- a/src/main/java/app/esiroi/auth/endpoint/security/TOTPConf.java
+++ b/src/main/java/app/esiroi/auth/endpoint/security/TOTPConf.java
@@ -21,8 +21,10 @@ public class TOTPConf {
private static final int DEFAULT_TOTP_DIGITS = 6;
private static final int DEFAULT_TOTP_PERIOD = 30; // in seconds
- public String generateTOTP(String secret) {
- return generateTOTP(secret, DEFAULT_TOTP_DIGITS, DEFAULT_TOTP_PERIOD);
+ public String getTotpUri(String secret, String account, String issuer) {
+ return String.format(
+ "otpauth://totp/%s:%s?secret=%s&issuer=%s&algorithm=SHA1&digits=6&period=30",
+ issuer, account, secret, issuer);
}
public String generateTOTP(String secret, int digits, int period) {
@@ -57,11 +59,7 @@ public String generateTOTP(String secret, int digits, long counter) {
}
public boolean validateTOTP(String secret, String otp) {
- return validateTOTP(secret, otp, DEFAULT_TOTP_DIGITS);
- }
-
- public boolean validateTOTP(String secret, String otp, int digits) {
- String expectedOTP = generateTOTP(secret, digits, DEFAULT_TOTP_PERIOD);
+ String expectedOTP = generateTOTP(secret, DEFAULT_TOTP_DIGITS, DEFAULT_TOTP_PERIOD);
return expectedOTP.equals(otp);
}
}
diff --git a/src/main/java/app/esiroi/auth/service/UserService.java b/src/main/java/app/esiroi/auth/service/AuthService.java
similarity index 63%
rename from src/main/java/app/esiroi/auth/service/UserService.java
rename to src/main/java/app/esiroi/auth/service/AuthService.java
index b0dea10..091bb21 100644
--- a/src/main/java/app/esiroi/auth/service/UserService.java
+++ b/src/main/java/app/esiroi/auth/service/AuthService.java
@@ -9,7 +9,14 @@
import app.esiroi.auth.model.exception.ForbiddenException;
import app.esiroi.auth.model.exception.NotFoundException;
import app.esiroi.auth.repository.UserRepository;
+import com.google.zxing.BarcodeFormat;
+import com.google.zxing.client.j2se.MatrixToImageWriter;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.qrcode.QRCodeWriter;
+import java.io.ByteArrayOutputStream;
+import java.util.Base64;
import lombok.AllArgsConstructor;
+import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -17,7 +24,7 @@
@Service
@AllArgsConstructor
@Slf4j
-public class UserService {
+public class AuthService {
private final UserRepository repository;
private final PasswordEncoder encoder;
private final JWTConf jwtConf;
@@ -57,4 +64,21 @@ public User validateOTP(String otp) {
}
throw new ForbiddenException("OTP Invalid");
}
+
+ @SneakyThrows
+ private String generateQrCode(String otpAuthUrl, int width, int height) {
+ QRCodeWriter qrCodeWriter = new QRCodeWriter();
+ BitMatrix bitMatrix = qrCodeWriter.encode(otpAuthUrl, BarcodeFormat.QR_CODE, width, height);
+ ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
+ MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
+ byte[] pngData = pngOutputStream.toByteArray();
+ return Base64.getEncoder().encodeToString(pngData); // Base64 pour HTML
+ }
+
+ public String setupTotp(String email) {
+ var authUser = getUserByEmail(email);
+ var decryptedSecret = new String(encryptor.getInstance().decrypt(authUser.getOtpSecret()));
+ String otpAuthUrl = totpConf.getTotpUri(decryptedSecret, authUser.getEmail(), "Authentify");
+ return generateQrCode(otpAuthUrl, 200, 200);
+ }
}
diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html
index ac72561..d01860e 100644
--- a/src/main/resources/templates/profile.html
+++ b/src/main/resources/templates/profile.html
@@ -57,7 +57,7 @@
Welcome
+
+
Scan
+
![QR Code TOTP]()
+
+
diff --git a/src/main/resources/templates/qrcode.html b/src/main/resources/templates/qrcode.html
new file mode 100644
index 0000000..421271f
--- /dev/null
+++ b/src/main/resources/templates/qrcode.html
@@ -0,0 +1,34 @@
+
+
+