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;
@RequiredArgsConstructor
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;
......
......@@ -3,6 +3,7 @@ package com.devteria.identityservice.controller;
import com.devteria.identityservice.dto.request.*;
import com.devteria.identityservice.dto.response.AuthenticationResponse;
import com.devteria.identityservice.dto.response.IntrospectResponse;
import com.devteria.identityservice.dto.response.LoginResponse;
import com.devteria.identityservice.service.AuthenticationService;
import com.nimbusds.jose.JOSEException;
import jakarta.mail.MessagingException;
......@@ -23,11 +24,17 @@ import java.text.ParseException;
public class AuthenticationController {
AuthenticationService authenticationService;
@PostMapping("/token")
ApiResponse<AuthenticationResponse> authenticate(@RequestBody AuthenticationRequest request) {
var result = authenticationService.authenticate(request);
@PostMapping("/login")
ApiResponse<LoginResponse> login(@RequestBody LoginRequest request) throws MessagingException {
return ApiResponse.<LoginResponse>builder()
.result(authenticationService.login(request))
.build();
}
@PostMapping("/verify-otp")
ApiResponse<AuthenticationResponse> verifyOtp(@RequestBody VerifyOtpLoginRequest request) {
return ApiResponse.<AuthenticationResponse>builder()
.result(result)
.result(authenticationService.verifyOtp(request))
.build();
}
......
......@@ -8,7 +8,7 @@ import lombok.experimental.FieldDefaults;
@AllArgsConstructor
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthenticationRequest {
String username;
public class LoginRequest {
String email;
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;
@Builder
@FieldDefaults(level = AccessLevel.PRIVATE)
public class AuthenticationResponse {
String token;
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;
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public enum OtpType {
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;
int expireTimeInSeconds;
......
......@@ -3,6 +3,7 @@ package com.devteria.identityservice.service;
import com.devteria.identityservice.dto.request.*;
import com.devteria.identityservice.dto.response.AuthenticationResponse;
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.User;
import com.devteria.identityservice.enums.OtpType;
......@@ -23,7 +24,6 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
......@@ -41,7 +41,7 @@ import java.util.concurrent.TimeUnit;
@Slf4j
@FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true)
public class AuthenticationService {
private static String TOKEN_KEY_PREFIX = "invalid_token:";
private static final String TOKEN_KEY_PREFIX = "invalid_token:";
RedisTemplate<String, Object> redisTemplate;
PasswordEncoder passwordEncoder;
OtpService otpService;
......@@ -78,9 +78,8 @@ public class AuthenticationService {
}
public AuthenticationResponse authenticate(AuthenticationRequest request) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(10);
var user = userRepository.findByUsername(request.getUsername())
public LoginResponse login(LoginRequest request) throws MessagingException {
var user = userRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new AppException(ErrorCode.USER_NOT_EXISTED));
boolean authenticated = passwordEncoder.matches(request.getPassword(),
......@@ -88,12 +87,33 @@ public class AuthenticationService {
if (!authenticated)
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);
return AuthenticationResponse.builder()
.token(token)
.authenticated(true)
.token(token)
.build();
}
private String generateToken(User user) {
......@@ -197,8 +217,8 @@ public class AuthenticationService {
saveInvalidatedToken(invalidatedToken);
String username = signedJWT.getJWTClaimsSet().getSubject();
User user = userRepository.findByUsername(username)
String userId = signedJWT.getJWTClaimsSet().getSubject();
User user = userRepository.findById(userId)
.orElseThrow(() -> new AppException(ErrorCode.UNAUTHENTICATED));
var token = generateToken(user);
......
......@@ -52,6 +52,9 @@ public class EmailService {
case REGISTRATION:
sendRegistrationOtp(to, otp);
break;
case LOGIN:
sendLoginOtp(to, otp);
break;
default:
log.error("Unknown OTP type: {}", otpType);
throw new IllegalArgumentException("Không hỗ trợ loại OTP này");
......@@ -72,6 +75,20 @@ public class EmailService {
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 {
String subject = "Mã OTP đổi mật khẩu của bạn";
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