Skip to content

Commit 08508c1

Browse files
Implementing 2FA using Google Authenticator
1 parent 2be2db0 commit 08508c1

21 files changed

+362
-153
lines changed

pom.xml

+15
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,21 @@
113113
<artifactId>spring-security-test</artifactId>
114114
<scope>test</scope>
115115
</dependency>
116+
<dependency>
117+
<groupId>com.warrenstrange</groupId>
118+
<artifactId>googleauth</artifactId>
119+
<version>1.4.0</version>
120+
</dependency>
121+
<dependency>
122+
<groupId>com.google.zxing</groupId>
123+
<artifactId>core</artifactId>
124+
<version>3.3.0</version>
125+
</dependency>
126+
<dependency>
127+
<groupId>com.google.zxing</groupId>
128+
<artifactId>javase</artifactId>
129+
<version>3.3.0</version>
130+
</dependency>
116131
</dependencies>
117132

118133
<build>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.bitscoderdotcom.link_generator_system.config;
2+
3+
import com.warrenstrange.googleauth.GoogleAuthenticator;
4+
import com.warrenstrange.googleauth.ICredentialRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import org.springframework.context.annotation.Bean;
7+
import org.springframework.context.annotation.Configuration;
8+
9+
@Configuration
10+
@RequiredArgsConstructor
11+
public class CustomGoogleAuthenticatorConfig {
12+
13+
private final ICredentialRepository credentialRepository;
14+
15+
@Bean
16+
public GoogleAuthenticator gAuth() {
17+
18+
GoogleAuthenticator googleAuthenticator = new GoogleAuthenticator();
19+
20+
googleAuthenticator.setCredentialRepository(credentialRepository);
21+
22+
return googleAuthenticator;
23+
24+
}
25+
}

src/main/java/com/bitscoderdotcom/link_generator_system/controller/AuthController.java

+22-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import com.bitscoderdotcom.link_generator_system.dto.ApiResponse;
44
import com.bitscoderdotcom.link_generator_system.dto.SignInRequest;
55
import com.bitscoderdotcom.link_generator_system.dto.UserRegistrationRequest;
6+
import com.bitscoderdotcom.link_generator_system.dto.ValidateCodeDto;
7+
import com.bitscoderdotcom.link_generator_system.entities.Validation;
68
import com.bitscoderdotcom.link_generator_system.security.service.AuthService;
79
import jakarta.servlet.http.Cookie;
810
import jakarta.servlet.http.HttpServletResponse;
@@ -57,8 +59,26 @@ public String signIn(@ModelAttribute SignInRequest request, HttpServletResponse
5759
cookie.setHttpOnly(true);
5860
// Add the cookie to the response
5961
response.addCookie(cookie);
60-
// Redirect to the generateInvoice page
61-
return "redirect:/lgsApp/v1/invoice/generateInvoice";
62+
// Redirect to the 2FA page
63+
return "redirect:/lgsApp/v1/auth/2fa";
64+
} else {
65+
redirectAttributes.addFlashAttribute("error", Objects.requireNonNull(apiResponse.getBody()).getMessage());
66+
return "redirect:/error";
67+
}
68+
}
69+
70+
@GetMapping("/2fa")
71+
public String show2FAForm(Model model) {
72+
model.addAttribute("validateCodeDto", new ValidateCodeDto());
73+
return "2fa";
74+
}
75+
76+
@PostMapping("/validate2FA")
77+
public String validate2FA(@ModelAttribute ValidateCodeDto body, RedirectAttributes redirectAttributes) {
78+
ResponseEntity<ApiResponse<SignInRequest.Response>> apiResponse = authService.validate2FA(body);
79+
if (apiResponse.getStatusCode() == HttpStatus.OK) {
80+
// Redirect to the Userpage
81+
return "redirect:/lgsApp/v1/userPage";
6282
} else {
6383
redirectAttributes.addFlashAttribute("error", Objects.requireNonNull(apiResponse.getBody()).getMessage());
6484
return "redirect:/error";

src/main/java/com/bitscoderdotcom/link_generator_system/controller/HomeController.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,6 @@ public class HomeController {
88

99
@GetMapping("/")
1010
public String home() {
11-
return "home";
11+
return "index";
1212
}
1313
}

src/main/java/com/bitscoderdotcom/link_generator_system/dto/EmailDetails.java

+3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import lombok.Data;
66
import lombok.NoArgsConstructor;
77

8+
import java.io.File;
9+
810
@AllArgsConstructor
911
@NoArgsConstructor
1012
@Builder
@@ -14,4 +16,5 @@ public class EmailDetails {
1416
private String recipient;
1517
private String subject;
1618
private String messageBody;
19+
private File attachment;
1720
}

src/main/java/com/bitscoderdotcom/link_generator_system/dto/SignInRequest.java

+2
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ public class SignInRequest {
1515
private String email;
1616
@NotBlank(message = "password should not be blank")
1717
private String password;
18+
@NotBlank(message = "authCode should not be blank")
19+
private String authCode;
1820

1921
@AllArgsConstructor
2022
@Getter
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.bitscoderdotcom.link_generator_system.dto;
2+
3+
import lombok.Data;
4+
import lombok.NoArgsConstructor;
5+
6+
@Data
7+
@NoArgsConstructor
8+
public class ValidateCodeDto {
9+
10+
private String username;
11+
private int verificationCode;
12+
}

src/main/java/com/bitscoderdotcom/link_generator_system/entities/Company.java

+3
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ public class Company {
2323
private String companyName;
2424
@OneToMany(mappedBy = "company", cascade = CascadeType.ALL)
2525
private List<Invoice> invoices;
26+
@OneToOne(cascade = CascadeType.ALL)
27+
@JoinColumn(name = "totp_id", referencedColumnName = "username")
28+
private UserTOTP userTOTP;
2629

2730
public Company() {
2831
this.setId(generateCustomUUID());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.bitscoderdotcom.link_generator_system.entities;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Data;
6+
import lombok.NoArgsConstructor;
7+
8+
import java.util.List;
9+
10+
@Entity
11+
@Table(name = "user_totp")
12+
@Data
13+
@NoArgsConstructor
14+
@AllArgsConstructor
15+
public class UserTOTP {
16+
17+
@Id
18+
private String username;
19+
20+
private String secretKey;
21+
22+
private int validationCode;
23+
24+
private List<Integer> scratchCodes;
25+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.bitscoderdotcom.link_generator_system.entities;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Data;
5+
import lombok.NoArgsConstructor;
6+
7+
@Data
8+
@AllArgsConstructor
9+
@NoArgsConstructor
10+
public class Validation {
11+
12+
private boolean isCodeValid;
13+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.bitscoderdotcom.link_generator_system.repository;
2+
3+
import com.bitscoderdotcom.link_generator_system.entities.UserTOTP;
4+
import com.warrenstrange.googleauth.ICredentialRepository;
5+
import org.springframework.stereotype.Component;
6+
7+
import java.util.HashMap;
8+
import java.util.List;
9+
import java.util.Map;
10+
11+
@Component
12+
public class CredentialRepository implements ICredentialRepository {
13+
14+
private final Map<String, UserTOTP> usersKeys = new HashMap<String, UserTOTP>();
15+
16+
@Override
17+
public String getSecretKey(String userName) {
18+
return usersKeys.get(userName).getSecretKey();
19+
}
20+
21+
@Override
22+
public void saveUserCredentials(String userName, String secretKey, int validationCode, List<Integer> scratchCodes) {
23+
24+
usersKeys.put(userName, new UserTOTP(userName, secretKey, validationCode, scratchCodes));
25+
}
26+
27+
public UserTOTP getUser(String username) {
28+
return usersKeys.get(username);
29+
}
30+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.bitscoderdotcom.link_generator_system.repository;
2+
3+
import com.bitscoderdotcom.link_generator_system.entities.UserTOTP;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
6+
public interface UserTOTPRepository extends JpaRepository<UserTOTP, String> {
7+
8+
UserTOTP findByUsername(String username);
9+
}

src/main/java/com/bitscoderdotcom/link_generator_system/security/service/AuthService.java

+87-8
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,24 @@
11
package com.bitscoderdotcom.link_generator_system.security.service;
22

3-
import com.bitscoderdotcom.link_generator_system.dto.ApiResponse;
4-
import com.bitscoderdotcom.link_generator_system.dto.EmailDetails;
5-
import com.bitscoderdotcom.link_generator_system.dto.SignInRequest;
6-
import com.bitscoderdotcom.link_generator_system.dto.UserRegistrationRequest;
3+
import com.bitscoderdotcom.link_generator_system.dto.*;
74
import com.bitscoderdotcom.link_generator_system.entities.Company;
5+
import com.bitscoderdotcom.link_generator_system.entities.UserTOTP;
6+
import com.bitscoderdotcom.link_generator_system.entities.Validation;
87
import com.bitscoderdotcom.link_generator_system.repository.CompanyRepository;
8+
import com.bitscoderdotcom.link_generator_system.repository.UserTOTPRepository;
99
import com.bitscoderdotcom.link_generator_system.security.jwt.JwtUtils;
1010
import com.bitscoderdotcom.link_generator_system.service.EmailService;
11+
import com.google.zxing.BarcodeFormat;
12+
import com.google.zxing.client.j2se.MatrixToImageWriter;
13+
import com.google.zxing.common.BitMatrix;
14+
import com.google.zxing.qrcode.QRCodeWriter;
15+
import com.warrenstrange.googleauth.GoogleAuthenticator;
16+
import com.warrenstrange.googleauth.GoogleAuthenticatorKey;
17+
import com.warrenstrange.googleauth.GoogleAuthenticatorQRGenerator;
18+
import jakarta.servlet.ServletOutputStream;
19+
import jakarta.servlet.http.HttpServletResponse;
1120
import lombok.AllArgsConstructor;
21+
import lombok.SneakyThrows;
1222
import lombok.extern.slf4j.Slf4j;
1323
import org.springframework.http.HttpStatus;
1424
import org.springframework.http.ResponseEntity;
@@ -22,7 +32,11 @@
2232
import org.springframework.transaction.annotation.Transactional;
2333
import org.springframework.web.server.ResponseStatusException;
2434

35+
import java.io.ByteArrayOutputStream;
36+
import java.io.File;
37+
import java.io.FileOutputStream;
2538
import java.time.LocalDateTime;
39+
import java.util.Base64;
2640
import java.util.UUID;
2741

2842
@Service
@@ -32,10 +46,12 @@ public class AuthService {
3246

3347
private AuthenticationManager authenticationManager;
3448
private CompanyRepository companyRepository;
49+
private UserTOTPRepository userTOTPRepository;
3550
private PasswordEncoder passwordEncoder;
3651
private JwtUtils jwtUtils;
3752
private UserDetailsServiceImpl userDetailsService;
3853
private EmailService emailService;
54+
private final GoogleAuthenticator gAuth;
3955

4056
@Transactional
4157
public String register(UserRegistrationRequest request) {
@@ -58,19 +74,31 @@ public String register(UserRegistrationRequest request) {
5874
return "Email Address already in use!";
5975
}
6076

77+
78+
log.info("Company registered successfully with username: {}", username);
79+
80+
GoogleAuthenticatorKey key = generate2faKey(username);
81+
82+
UserTOTP userTOTP = new UserTOTP();
83+
userTOTP.setUsername(username);
84+
userTOTP.setSecretKey(key.getKey());
85+
userTOTPRepository.save(userTOTP);
86+
6187
Company company = new Company();
6288
company.setUserName(request.getName());
6389
company.setCompanyName(request.getCompanyName());
6490
company.setCompanyEmail(request.getEmail());
6591
company.setPassword(passwordEncoder.encode(request.getPassword()));
92+
company.setUserTOTP(userTOTP);
6693
companyRepository.save(company);
6794

68-
log.info("Company registered successfully with username: {}", username);
69-
7095
EmailDetails emailDetails = new EmailDetails();
7196
emailDetails.setRecipient(company.getCompanyEmail());
7297
emailDetails.setSubject("Account Registration Confirmation");
73-
emailDetails.setMessageBody("Your account has been registered on our platform");
98+
emailDetails.setMessageBody("Your account has been registered on our platform.\n" +
99+
"Please scan the following QR code with your Google Authenticator app to enable 2FA.\n" +
100+
"Key: " + key.getKey());
101+
emailDetails.setAttachment(generateQRCode(username, key));
74102
emailService.sendEmail(emailDetails);
75103

76104
return "Company registered successfully";
@@ -98,13 +126,64 @@ public ResponseEntity<ApiResponse<SignInRequest.Response>> signIn(SignInRequest
98126
jwtUtils.getJwtExpirationDate()
99127
);
100128

101-
return createSuccessResponse("Company signed in successfully", response);
129+
return createSuccessResponse("2FA required", response);
102130
} catch (BadCredentialsException e) {
103131
log.info("Invalid email or password for email: {}", request.getEmail());
104132
return createBadRequestResponse("Invalid email or password", null);
105133
}
106134
}
107135

136+
// @SneakyThrows
137+
// public File generate2fa(String username) {
138+
// final GoogleAuthenticatorKey key = gAuth.createCredentials(username);
139+
//
140+
// UserTOTP userTOTP = new UserTOTP();
141+
// userTOTP.setUsername(username);
142+
// userTOTP.setSecretKey(key.getKey());
143+
// userTOTPRepository.save(userTOTP);
144+
//
145+
// QRCodeWriter qrCodeWriter = new QRCodeWriter();
146+
// String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL("Link Generation App", username, key);
147+
// BitMatrix bitMatrix = qrCodeWriter.encode(otpAuthURL, BarcodeFormat.QR_CODE, 200, 200);
148+
//
149+
// File qrFile = File.createTempFile("qrcode", ".png");
150+
// FileOutputStream pngOutputStream = new FileOutputStream(qrFile);
151+
// MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
152+
// pngOutputStream.close();
153+
//
154+
// return qrFile;
155+
// }
156+
@SneakyThrows
157+
public GoogleAuthenticatorKey generate2faKey(String username) {
158+
return gAuth.createCredentials(username);
159+
}
160+
161+
@SneakyThrows
162+
public File generateQRCode(String username, GoogleAuthenticatorKey key) {
163+
String otpAuthURL = GoogleAuthenticatorQRGenerator.getOtpAuthTotpURL("Link Generation App", username, key);
164+
BitMatrix bitMatrix = new QRCodeWriter().encode(otpAuthURL, BarcodeFormat.QR_CODE, 200, 200);
165+
166+
File qrFile = File.createTempFile("qrcode", ".png");
167+
try (FileOutputStream pngOutputStream = new FileOutputStream(qrFile)) {
168+
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
169+
}
170+
171+
return qrFile;
172+
}
173+
174+
public ResponseEntity<ApiResponse<SignInRequest.Response>> validate2FA(ValidateCodeDto body) {
175+
log.info("validate2FA method called with username: {}", body.getUsername());
176+
177+
UserTOTP userTOTP = userTOTPRepository.findByUsername(body.getUsername());
178+
179+
if (userTOTP == null || !gAuth.authorizeUser(body.getUsername(), body.getVerificationCode())) {
180+
log.info("Invalid 2FA code for username: {}", body.getUsername());
181+
return createBadRequestResponse("Invalid 2FA code", null);
182+
}
183+
184+
return createSuccessResponse("User authenticated successfully", null);
185+
}
186+
108187
public <T> ResponseEntity<ApiResponse<T>> createSuccessResponse(String message, T data) {
109188
return ResponseEntity.ok(new ApiResponse<>(
110189
LocalDateTime.now(),

src/main/java/com/bitscoderdotcom/link_generator_system/service/EmailService.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,17 @@ public class EmailService {
2323
public void sendEmail (EmailDetails emailDetails) {
2424
try {
2525
MimeMessage mimeMessage = mailSender.createMimeMessage();
26-
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "utf-8");
26+
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true, "utf-8");
2727

2828
helper.setFrom(emailSender);
2929
helper.setTo(emailDetails.getRecipient());
3030
helper.setSubject(emailDetails.getSubject());
3131
helper.setText(emailDetails.getMessageBody(), true);
3232

33+
if (emailDetails.getAttachment() != null) {
34+
helper.addAttachment("QRCode.png", emailDetails.getAttachment());
35+
}
36+
3337
mailSender.send(mimeMessage);
3438
log.info("Message sent to: {}", emailDetails.getRecipient());
3539
log.info("Message sender: {}", emailSender);

0 commit comments

Comments
 (0)