From 390e5b83151d149b0453850267765fbfd8aeb717 Mon Sep 17 00:00:00 2001 From: Jewel Rana Date: Sun, 15 Mar 2026 09:02:25 +0000 Subject: [PATCH] feat: add TOTP (Google Authenticator) verification for product endpoints - Add dev.samstevens.totp dependency for TOTP support - Create TotpUser entity to store username and TOTP secret - Create TotpUserRepository for database access - Create TotpService with secret generation, QR code URI, and OTP verification - Create TotpController with /totp/setup and /totp/verify endpoints - Add DTOs for TOTP setup response and verify request - Guard product endpoints with OTP verification (username + otp params required) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- pom.xml | 7 ++ .../controller/RelatedProductController.java | 53 +++++++--- .../controller/TotpController.java | 57 ++++++++++ .../dto/TotpSetupResponse.java | 13 +++ .../dto/TotpVerifyRequest.java | 11 ++ .../model/TotpUser.java | 28 +++++ .../repository/TotpUserRepository.java | 12 +++ .../service/TotpService.java | 100 ++++++++++++++++++ 8 files changed, 269 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/rana/performance_engineering_1/controller/TotpController.java create mode 100644 src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java create mode 100644 src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java create mode 100644 src/main/java/com/rana/performance_engineering_1/model/TotpUser.java create mode 100644 src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java create mode 100644 src/main/java/com/rana/performance_engineering_1/service/TotpService.java diff --git a/pom.xml b/pom.xml index f814f1f..6eed748 100644 --- a/pom.xml +++ b/pom.xml @@ -60,6 +60,13 @@ spring-boot-starter-test test + + + + dev.samstevens.totp + totp + 1.7.1 + diff --git a/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java b/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java index fd57a9f..96928ab 100644 --- a/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java +++ b/src/main/java/com/rana/performance_engineering_1/controller/RelatedProductController.java @@ -3,15 +3,14 @@ import com.rana.performance_engineering_1.model.Product; import com.rana.performance_engineering_1.service.ProductSearchService; import com.rana.performance_engineering_1.service.RelatedProductService; +import com.rana.performance_engineering_1.service.TotpService; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.util.StopWatch; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; +import java.util.Map; import java.util.Optional; @RestController @@ -20,12 +19,28 @@ public class RelatedProductController { private final RelatedProductService relatedProductService; private final ProductSearchService productSearchService; + private final TotpService totpService; + /** + * GET /products/{productId}?username=john&otp=123456 + * Requires valid TOTP OTP to access the product. + */ @GetMapping("/products/{productId}") - public ResponseEntity getProducts(@PathVariable Long productId) { - - // 1. Create and start the stopwatch + public ResponseEntity getProducts( + @PathVariable Long productId, + @RequestParam String username, + @RequestParam String otp) { + + // Verify OTP before returning the product + if (!totpService.verifyOtp(username, otp)) { + return ResponseEntity.status(401).body(Map.of( + "error", "Invalid or expired OTP.", + "message", "Please provide a valid OTP from Google Authenticator." + )); + } + + // OTP is valid — proceed to fetch the product StopWatch stopWatch = new StopWatch("RelatedProducts"); stopWatch.start(); @@ -38,11 +53,25 @@ public ResponseEntity getProducts(@PathVariable Long productId) { } - + /** + * GET /products/{productId}/related?username=john&otp=123456 + * Requires valid TOTP OTP to access related products. + */ @GetMapping("/products/{productId}/related") - public List getRelatedProducts(@PathVariable Long productId) { - - // 1. Create and start the stopwatch + public ResponseEntity getRelatedProducts( + @PathVariable Long productId, + @RequestParam String username, + @RequestParam String otp) { + + // Verify OTP before returning related products + if (!totpService.verifyOtp(username, otp)) { + return ResponseEntity.status(401).body(Map.of( + "error", "Invalid or expired OTP.", + "message", "Please provide a valid OTP from Google Authenticator." + )); + } + + // OTP is valid — proceed StopWatch stopWatch = new StopWatch("RelatedProducts"); stopWatch.start(); @@ -51,7 +80,7 @@ public List getRelatedProducts(@PathVariable Long productId) { System.out.println(stopWatch.prettyPrint()); - return relatedProducts; + return ResponseEntity.ok(relatedProducts); } @GetMapping("/products/autocomplete") diff --git a/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java b/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java new file mode 100644 index 0000000..04853a8 --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/controller/TotpController.java @@ -0,0 +1,57 @@ +package com.rana.performance_engineering_1.controller; + +import com.rana.performance_engineering_1.dto.TotpSetupResponse; +import com.rana.performance_engineering_1.dto.TotpVerifyRequest; +import com.rana.performance_engineering_1.service.TotpService; +import dev.samstevens.totp.exceptions.QrGenerationException; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/totp") +@RequiredArgsConstructor +public class TotpController { + + private final TotpService totpService; + + /** + * POST /totp/setup?username=john + * Registers a user for TOTP and returns the secret + QR code data URI. + * The QR code can be scanned with Google Authenticator. + */ + @PostMapping("/setup") + public ResponseEntity setupTotp(@RequestParam String username) { + try { + TotpSetupResponse response = totpService.setupTotp(username); + return ResponseEntity.ok(response); + } catch (QrGenerationException e) { + return ResponseEntity.internalServerError() + .body(Map.of("error", "Failed to generate QR code: " + e.getMessage())); + } + } + + /** + * POST /totp/verify + * Verifies the OTP code provided by the user. + * Body: { "username": "john", "otp": "123456" } + */ + @PostMapping("/verify") + public ResponseEntity verifyOtp(@RequestBody TotpVerifyRequest request) { + boolean isValid = totpService.verifyOtp(request.getUsername(), request.getOtp()); + + if (isValid) { + return ResponseEntity.ok(Map.of( + "valid", true, + "message", "OTP verification successful." + )); + } else { + return ResponseEntity.status(401).body(Map.of( + "valid", false, + "message", "Invalid OTP. Please try again." + )); + } + } +} diff --git a/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java b/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java new file mode 100644 index 0000000..b484710 --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/dto/TotpSetupResponse.java @@ -0,0 +1,13 @@ +package com.rana.performance_engineering_1.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class TotpSetupResponse { + private String username; + private String secret; + private String qrCodeUri; + private String message; +} diff --git a/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java b/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java new file mode 100644 index 0000000..8f2046b --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/dto/TotpVerifyRequest.java @@ -0,0 +1,11 @@ +package com.rana.performance_engineering_1.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class TotpVerifyRequest { + private String username; + private String otp; +} diff --git a/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java b/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java new file mode 100644 index 0000000..a190bec --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/model/TotpUser.java @@ -0,0 +1,28 @@ +package com.rana.performance_engineering_1.model; + +import jakarta.persistence.*; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Entity +@Getter +@Setter +@NoArgsConstructor +public class TotpUser { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(unique = true, nullable = false) + private String username; + + @Column(nullable = false) + private String secret; + + public TotpUser(String username, String secret) { + this.username = username; + this.secret = secret; + } +} diff --git a/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java b/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java new file mode 100644 index 0000000..8d66636 --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/repository/TotpUserRepository.java @@ -0,0 +1,12 @@ +package com.rana.performance_engineering_1.repository; + +import com.rana.performance_engineering_1.model.TotpUser; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface TotpUserRepository extends JpaRepository { + Optional findByUsername(String username); +} diff --git a/src/main/java/com/rana/performance_engineering_1/service/TotpService.java b/src/main/java/com/rana/performance_engineering_1/service/TotpService.java new file mode 100644 index 0000000..b3bd15a --- /dev/null +++ b/src/main/java/com/rana/performance_engineering_1/service/TotpService.java @@ -0,0 +1,100 @@ +package com.rana.performance_engineering_1.service; + +import com.rana.performance_engineering_1.dto.TotpSetupResponse; +import com.rana.performance_engineering_1.model.TotpUser; +import com.rana.performance_engineering_1.repository.TotpUserRepository; +import dev.samstevens.totp.code.*; +import dev.samstevens.totp.exceptions.QrGenerationException; +import dev.samstevens.totp.qr.QrData; +import dev.samstevens.totp.qr.ZxingPngQrGenerator; +import dev.samstevens.totp.secret.DefaultSecretGenerator; +import dev.samstevens.totp.secret.SecretGenerator; +import dev.samstevens.totp.time.SystemTimeProvider; +import dev.samstevens.totp.time.TimeProvider; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +import static dev.samstevens.totp.util.Utils.getDataUriForImage; + +@Service +@RequiredArgsConstructor +public class TotpService { + + private final TotpUserRepository totpUserRepository; + + /** + * Registers a new user and generates a TOTP secret. + * Returns the secret and a QR code URI that can be scanned by Google Authenticator. + */ + public TotpSetupResponse setupTotp(String username) throws QrGenerationException { + // Check if user already exists + Optional existingUser = totpUserRepository.findByUsername(username); + if (existingUser.isPresent()) { + // Re-generate setup info for existing user + String secret = existingUser.get().getSecret(); + String qrCodeUri = generateQrCodeUri(username, secret); + return new TotpSetupResponse(username, secret, qrCodeUri, + "User already registered. Use the existing secret or scan the QR code again."); + } + + // Generate a new secret + SecretGenerator secretGenerator = new DefaultSecretGenerator(); + String secret = secretGenerator.generate(); + + // Save user with secret + TotpUser totpUser = new TotpUser(username, secret); + totpUserRepository.save(totpUser); + + // Generate QR code URI + String qrCodeUri = generateQrCodeUri(username, secret); + + return new TotpSetupResponse(username, secret, qrCodeUri, + "TOTP setup successful. Scan the QR code with Google Authenticator."); + } + + /** + * Verifies the OTP code provided by the user. + */ + public boolean verifyOtp(String username, String otp) { + Optional totpUser = totpUserRepository.findByUsername(username); + if (totpUser.isEmpty()) { + return false; + } + + String secret = totpUser.get().getSecret(); + + TimeProvider timeProvider = new SystemTimeProvider(); + CodeGenerator codeGenerator = new DefaultCodeGenerator(); + CodeVerifier verifier = new DefaultCodeVerifier(codeGenerator, timeProvider); + + return verifier.isValidCode(secret, otp); + } + + /** + * Checks if a user is registered for TOTP. + */ + public boolean isUserRegistered(String username) { + return totpUserRepository.findByUsername(username).isPresent(); + } + + /** + * Generates a QR code data URI for scanning with Google Authenticator. + */ + private String generateQrCodeUri(String username, String secret) throws QrGenerationException { + QrData qrData = new QrData.Builder() + .label(username) + .secret(secret) + .issuer("PerformanceEngineeringApp") + .algorithm(HashingAlgorithm.SHA1) + .digits(6) + .period(30) + .build(); + + ZxingPngQrGenerator qrGenerator = new ZxingPngQrGenerator(); + byte[] imageData = qrGenerator.generate(qrData); + + return getDataUriForImage(imageData, qrGenerator.getImageMimeType()); + } +}