Commit bf445329 authored by Phùng Quốc Toàn's avatar Phùng Quốc Toàn

feat: implement login and OTP verification functionality

parent 58a1b9d6
...@@ -20,7 +20,7 @@ import org.springframework.security.web.SecurityFilterChain; ...@@ -20,7 +20,7 @@ import org.springframework.security.web.SecurityFilterChain;
@RequiredArgsConstructor @RequiredArgsConstructor
public class SecurityConfig { public class SecurityConfig {
private final String[] PUBLIC_ENDPOINTS = new String[]{"/auth/token", "/auth/introspect", "/users", "/auth/logout", "/auth/refresh-token", "/auth/forgot-password", "/auth/reset-password"}; private final String[] PUBLIC_ENDPOINTS = new String[]{"/auth/login", "/auth/introspect", "/users", "/auth/logout", "/auth/refresh-token", "/auth/forgot-password", "/auth/reset-password", "/auth/verify-otp"};
CustomJwtDecoder customJwtDecoder; CustomJwtDecoder customJwtDecoder;
......
...@@ -3,6 +3,7 @@ package com.devteria.identityservice.controller; ...@@ -3,6 +3,7 @@ package com.devteria.identityservice.controller;
import com.devteria.identityservice.dto.request.*; import com.devteria.identityservice.dto.request.*;
import com.devteria.identityservice.dto.response.AuthenticationResponse; import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse; import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.dto.response.LoginResponse;
import com.devteria.identityservice.service.AuthenticationService; import com.devteria.identityservice.service.AuthenticationService;
import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JOSEException;
import jakarta.mail.MessagingException; import jakarta.mail.MessagingException;
...@@ -23,11 +24,17 @@ import java.text.ParseException; ...@@ -23,11 +24,17 @@ import java.text.ParseException;
public class AuthenticationController { public class AuthenticationController {
AuthenticationService authenticationService; AuthenticationService authenticationService;
@PostMapping("/token") @PostMapping("/login")
ApiResponse<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) { ApiResponse<LoginResponse> login(@RequestBody LoginRequest request) throws MessagingException {
var result = authenticationService.authenticate(request); return ApiResponse.<LoginResponse>builder()
.result(authenticationService.login(request))
.build();
}
@PostMapping("/verify-otp")
ApiResponse<AuthenticationResponse> verifyOtp(@RequestBody VerifyOtpLoginRequest request) {
return ApiResponse.<AuthenticationResponse>builder() return ApiResponse.<AuthenticationResponse>builder()
.result(result) .result(authenticationService.verifyOtp(request))
.build(); .build();
} }
......
...@@ -8,7 +8,7 @@ import lombok.experimental.FieldDefaults; ...@@ -8,7 +8,7 @@ import lombok.experimental.FieldDefaults;
@AllArgsConstructor @AllArgsConstructor
@Builder @Builder
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthenticationRequest { public class LoginRequest {
String username; String email;
String password; String password;
} }
package com.devteria.identityservice.dto.request;
import lombok.*;
import lombok.experimental.FieldDefaults;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class VerifyOtpLoginRequest {
String email;
String otp;
}
...@@ -9,6 +9,6 @@ import lombok.experimental.FieldDefaults; ...@@ -9,6 +9,6 @@ import lombok.experimental.FieldDefaults;
@Builder @Builder
@FieldDefaults(level = AccessLevel.PRIVATE) @FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthenticationResponse { public class AuthenticationResponse {
String token;
boolean authenticated; boolean authenticated;
String token;
} }
package com.devteria.identityservice.dto.response;
import lombok.*;
import lombok.experimental.FieldDefaults;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class LoginResponse {
boolean authenticated;
boolean requiredOtp;
String sentTo;
int resendAfter;
}
...@@ -10,7 +10,8 @@ import lombok.experimental.FieldDefaults; ...@@ -10,7 +10,8 @@ import lombok.experimental.FieldDefaults;
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public enum OtpType { public enum OtpType {
RESET_PASSWORD("reset-password:", 2 * 60), // 2 phút RESET_PASSWORD("reset-password:", 2 * 60), // 2 phút
REGISTRATION("registration:", 5 * 60); // 5 phút REGISTRATION("registration:", 5 * 60), // 5 phút
LOGIN("login:", 5 * 60); // 5 phút
String name; String name;
int expireTimeInSeconds; int expireTimeInSeconds;
......
...@@ -3,6 +3,7 @@ package com.devteria.identityservice.service; ...@@ -3,6 +3,7 @@ package com.devteria.identityservice.service;
import com.devteria.identityservice.dto.request.*; import com.devteria.identityservice.dto.request.*;
import com.devteria.identityservice.dto.response.AuthenticationResponse; import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse; import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.dto.response.LoginResponse;
import com.devteria.identityservice.entity.InvalidatedToken; import com.devteria.identityservice.entity.InvalidatedToken;
import com.devteria.identityservice.entity.User; import com.devteria.identityservice.entity.User;
import com.devteria.identityservice.enums.OtpType; import com.devteria.identityservice.enums.OtpType;
...@@ -23,7 +24,6 @@ import lombok.extern.slf4j.Slf4j; ...@@ -23,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
...@@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit; ...@@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j @Slf4j
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class AuthenticationService { public class AuthenticationService {
private static String TOKEN_KEY_PREFIX = "invalid_token:"; private static final String TOKEN_KEY_PREFIX = "invalid_token:";
RedisTemplate<String, Object> redisTemplate; RedisTemplate<String, Object> redisTemplate;
PasswordEncoder passwordEncoder; PasswordEncoder passwordEncoder;
OtpService otpService; OtpService otpService;
...@@ -78,9 +78,8 @@ public class AuthenticationService { ...@@ -78,9 +78,8 @@ public class AuthenticationService {
} }
public AuthenticationResponse authenticate(AuthenticationRequest request) { public LoginResponse login(LoginRequest request) throws MessagingException {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10); var user = userRepository.findByEmail(request.getEmail())
var user = userRepository.findByUsername(request.getUsername())
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED)); .orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED));
boolean authenticated = passwordEncoder.matches(request.getPassword(), boolean authenticated = passwordEncoder.matches(request.getPassword(),
...@@ -88,12 +87,33 @@ public class AuthenticationService { ...@@ -88,12 +87,33 @@ public class AuthenticationService {
if (!authenticated) if (!authenticated)
throw new AppException(ErrorCode.UNAUTHENTICATED); throw new AppException(ErrorCode.UNAUTHENTICATED);
String otp = otpService.generateAndSaveOtp(request.getEmail(), OtpType.LOGIN);
emailService.sendOtpEmail(request.getEmail(), otp, OtpType.LOGIN);
return LoginResponse.builder()
.authenticated(false)
.requiredOtp(true)
.sentTo(request.getEmail())
.resendAfter(OtpType.LOGIN.getExpireTimeInSeconds())
.build();
}
public AuthenticationResponse verifyOtp(VerifyOtpLoginRequest request) {
if (!(otpService.validateOtp(request.getOtp(), request.getEmail(), OtpType.LOGIN))) {
throw new AppException(ErrorCode.INVALID_OTP);
}
User user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new AppException(ErrorCode.UNAUTHENTICATED));
var token = generateToken(user); var token = generateToken(user);
return AuthenticationResponse.builder() return AuthenticationResponse.builder()
.token(token)
.authenticated(true) .authenticated(true)
.token(token)
.build(); .build();
} }
private String generateToken(User user) { private String generateToken(User user) {
...@@ -197,8 +217,8 @@ public class AuthenticationService { ...@@ -197,8 +217,8 @@ public class AuthenticationService {
saveInvalidatedToken(invalidatedToken); saveInvalidatedToken(invalidatedToken);
String username = signedJWT.getJWTClaimsSet().getSubject(); String userId = signedJWT.getJWTClaimsSet().getSubject();
User user = userRepository.findByUsername(username) User user = userRepository.findById(userId)
.orElseThrow(() -> new AppException(ErrorCode.UNAUTHENTICATED)); .orElseThrow(() -> new AppException(ErrorCode.UNAUTHENTICATED));
var token = generateToken(user); var token = generateToken(user);
......
...@@ -52,6 +52,9 @@ public class EmailService { ...@@ -52,6 +52,9 @@ public class EmailService {
case REGISTRATION: case REGISTRATION:
sendRegistrationOtp(to, otp); sendRegistrationOtp(to, otp);
break; break;
case LOGIN:
sendLoginOtp(to, otp);
break;
default: default:
log.error("Unknown OTP type: {}", otpType); log.error("Unknown OTP type: {}", otpType);
throw new IllegalArgumentException("Không hỗ trợ loại OTP này"); throw new IllegalArgumentException("Không hỗ trợ loại OTP này");
...@@ -72,6 +75,20 @@ public class EmailService { ...@@ -72,6 +75,20 @@ public class EmailService {
sendEmail(to, subject, htmlContent, true); sendEmail(to, subject, htmlContent, true);
} }
private void sendLoginOtp(String to, String otp) throws MessagingException {
String subject = "Mã OTP đăng nhập của bạn";
String htmlContent = String.format(
"<div style='font-family: Arial, sans-serif;'>" +
"<h2>Đăng nhập tài khoản</h2>" +
"<p>Mã OTP của bạn để đăng nhập là:</p>" +
"<h1 style='color: #4285f4; font-size: 32px; letter-spacing: 2px;'>%s</h1>" +
"<p>Mã này có hiệu lực trong 5 phút.</p>" +
"<p>Nếu bạn không yêu cầu đăng nhập, vui lòng bỏ qua email này.</p>" +
"</div>", otp);
sendEmail(to, subject, htmlContent, true);
}
private void sendPasswordResetOtp(String to, String otp) throws MessagingException { private void sendPasswordResetOtp(String to, String otp) throws MessagingException {
String subject = "Mã OTP đổi mật khẩu của bạn"; String subject = "Mã OTP đổi mật khẩu của bạn";
String htmlContent = String.format( String htmlContent = String.format(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment