fix: implement refresh token functionality and update JWT configuration #8
@ -38,8 +38,7 @@ public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
var authProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
|
||||
@ -1,9 +1,6 @@
|
||||
package com.magamochi.mangamochi.controller;
|
||||
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.DefaultResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.*;
|
||||
import com.magamochi.mangamochi.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@ -27,6 +24,17 @@ public class AuthenticationController {
|
||||
return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Refresh authentication token",
|
||||
description = "Refresh the authentication token",
|
||||
tags = {"Auth"},
|
||||
operationId = "refreshAuthToken")
|
||||
@PostMapping("/refresh")
|
||||
public DefaultResponseDTO<AuthenticationResponseDTO> refreshAuthToken(
|
||||
@RequestBody RefreshTokenRequestDTO authenticationRequestDTO) {
|
||||
return DefaultResponseDTO.ok(userService.refreshAuthToken(authenticationRequestDTO));
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "Register user",
|
||||
description = "Register a new user.",
|
||||
|
||||
@ -5,7 +5,8 @@ import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record AuthenticationResponseDTO(
|
||||
@NotNull Long id,
|
||||
@NotNull String token,
|
||||
@NotNull String accessToken,
|
||||
@NotNull String refreshToken,
|
||||
@NotNull String email,
|
||||
@NotNull String name,
|
||||
@NotNull UserRole role) {}
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
package com.magamochi.mangamochi.model.dto;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
public record RefreshTokenRequestDTO(@NotNull String refreshToken) {}
|
||||
@ -1,5 +1,8 @@
|
||||
package com.magamochi.mangamochi.security;
|
||||
|
||||
import static java.util.Objects.isNull;
|
||||
import static java.util.Objects.nonNull;
|
||||
|
||||
import com.magamochi.mangamochi.service.CustomUserDetailsService;
|
||||
import com.magamochi.mangamochi.util.JwtUtil;
|
||||
import jakarta.servlet.FilterChain;
|
||||
@ -8,9 +11,9 @@ import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@ -18,40 +21,46 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtRequestFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtUtil jwtUtil;
|
||||
private final CustomUserDetailsService userDetailsService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(
|
||||
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||
HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain)
|
||||
throws ServletException, IOException {
|
||||
|
||||
final String authorizationHeader = request.getHeader("Authorization");
|
||||
|
||||
String username = null;
|
||||
String jwt = null;
|
||||
|
||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
||||
jwt = authorizationHeader.substring(7);
|
||||
if (nonNull(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) {
|
||||
var jwt = authorizationHeader.substring(7);
|
||||
try {
|
||||
username = jwtUtil.extractUsername(jwt);
|
||||
var username = jwtUtil.extractUsername(jwt);
|
||||
|
||||
if (nonNull(username) && isNull(SecurityContextHolder.getContext().getAuthentication())) {
|
||||
authenticateUser(username, jwt, request, response);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
logger.warn("JWT token validation failed", e);
|
||||
response.sendError(
|
||||
HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired authorization token.");
|
||||
}
|
||||
}
|
||||
|
||||
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (jwtUtil.validateToken(jwt, userDetails)) {
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userDetails, null, userDetails.getAuthorities());
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
}
|
||||
}
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private void authenticateUser(
|
||||
String username, String jwt, HttpServletRequest request, HttpServletResponse response)
|
||||
throws IOException {
|
||||
var userDetails = this.userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (!jwtUtil.validateAccessToken(jwt, userDetails)) {
|
||||
response.sendError(
|
||||
HttpServletResponse.SC_UNAUTHORIZED, "Invalid or expired authorization token.");
|
||||
return;
|
||||
}
|
||||
|
||||
var authToken =
|
||||
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
|
||||
authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import com.magamochi.mangamochi.exception.ConflictException;
|
||||
import com.magamochi.mangamochi.exception.NotFoundException;
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
|
||||
import com.magamochi.mangamochi.model.dto.RefreshTokenRequestDTO;
|
||||
import com.magamochi.mangamochi.model.dto.RegistrationRequestDTO;
|
||||
import com.magamochi.mangamochi.model.entity.User;
|
||||
import com.magamochi.mangamochi.model.enumeration.UserRole;
|
||||
@ -16,7 +17,6 @@ import lombok.extern.log4j.Log4j2;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.BadCredentialsException;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@ -33,25 +33,47 @@ public class UserService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
|
||||
var token = new UsernamePasswordAuthenticationToken(request.email(), request.password());
|
||||
|
||||
try {
|
||||
authenticationManager.authenticate(token);
|
||||
} catch (AuthenticationException e) {
|
||||
if (e.getMessage().equals("Bad credentials")) {
|
||||
throw new BadCredentialsException("Wrong email or password.");
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
authenticationManager.authenticate(
|
||||
new UsernamePasswordAuthenticationToken(request.email(), request.password()));
|
||||
|
||||
var userDetails = userDetailsService.loadUserByUsername(request.email());
|
||||
var jwt = jwtUtil.generateToken(userDetails);
|
||||
|
||||
var accessToken = jwtUtil.generateAccessToken(userDetails);
|
||||
var refreshToken = jwtUtil.generateRefreshToken(userDetails);
|
||||
|
||||
var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
|
||||
|
||||
return new AuthenticationResponseDTO(
|
||||
user.getId(), jwt, userDetails.getUsername(), user.getName(), user.getRole());
|
||||
user.getId(),
|
||||
accessToken,
|
||||
refreshToken,
|
||||
userDetails.getUsername(),
|
||||
user.getName(),
|
||||
user.getRole());
|
||||
}
|
||||
|
||||
public AuthenticationResponseDTO refreshAuthToken(
|
||||
RefreshTokenRequestDTO authenticationRequestDTO) {
|
||||
var username = jwtUtil.extractUsernameFromRefreshToken(authenticationRequestDTO.refreshToken());
|
||||
|
||||
var userDetails = userDetailsService.loadUserByUsername(username);
|
||||
|
||||
if (!jwtUtil.validateRefreshToken(authenticationRequestDTO.refreshToken(), userDetails)) {
|
||||
throw new BadCredentialsException("Invalid refresh token");
|
||||
}
|
||||
|
||||
var newAccessToken = jwtUtil.generateAccessToken(userDetails);
|
||||
var newRefreshToken = jwtUtil.generateRefreshToken(userDetails);
|
||||
|
||||
var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
|
||||
|
||||
return new AuthenticationResponseDTO(
|
||||
user.getId(),
|
||||
newAccessToken,
|
||||
newRefreshToken,
|
||||
userDetails.getUsername(),
|
||||
user.getName(),
|
||||
user.getRole());
|
||||
}
|
||||
|
||||
public void register(RegistrationRequestDTO request) {
|
||||
|
||||
@ -2,13 +2,13 @@ package com.magamochi.mangamochi.util;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.io.Decoders;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import java.security.Key;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import javax.crypto.SecretKey;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Component;
|
||||
@ -18,51 +18,84 @@ public class JwtUtil {
|
||||
@Value("${jwt.secret}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.refresh-secret}")
|
||||
private String refreshSecret;
|
||||
|
||||
@Value("${jwt.expiration}")
|
||||
private Long expiration;
|
||||
|
||||
private Key getSigningKey() {
|
||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
||||
}
|
||||
@Value("${jwt.refresh-expiration}")
|
||||
private Long refreshExpiration;
|
||||
|
||||
public String extractUsername(String token) {
|
||||
return extractClaim(token, Claims::getSubject);
|
||||
return extractClaim(token, Claims::getSubject, getAccessSigningKey());
|
||||
}
|
||||
|
||||
public Date extractExpiration(String token) {
|
||||
return extractClaim(token, Claims::getExpiration);
|
||||
}
|
||||
public String extractUsernameFromRefreshToken(String token) {
|
||||
return extractClaim(token, Claims::getSubject, getRefreshSigningKey());
|
||||
}
|
||||
|
||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
||||
final Claims claims = extractAllClaims(token);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private Claims extractAllClaims(String token) {
|
||||
return Jwts.parser().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody();
|
||||
}
|
||||
|
||||
private Boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token).before(new Date());
|
||||
}
|
||||
|
||||
public String generateToken(UserDetails userDetails) {
|
||||
public String generateAccessToken(UserDetails userDetails) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
return createToken(claims, userDetails.getUsername());
|
||||
return createToken(claims, userDetails.getUsername(), expiration, getAccessSigningKey());
|
||||
}
|
||||
|
||||
private String createToken(Map<String, Object> claims, String subject) {
|
||||
public String generateRefreshToken(UserDetails userDetails) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
return createToken(
|
||||
claims, userDetails.getUsername(), refreshExpiration, getRefreshSigningKey());
|
||||
}
|
||||
|
||||
private String createToken(
|
||||
Map<String, Object> claims, String subject, Long duration, SecretKey key) {
|
||||
var now = new Date();
|
||||
var expiry = new Date(now.getTime() + duration);
|
||||
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(subject)
|
||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
||||
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
|
||||
.claims(claims)
|
||||
.subject(subject)
|
||||
.issuedAt(now)
|
||||
.expiration(expiry)
|
||||
.signWith(key)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
||||
final String username = extractUsername(token);
|
||||
public Boolean validateAccessToken(String token, UserDetails userDetails) {
|
||||
var username = extractUsername(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||
}
|
||||
|
||||
public boolean validateRefreshToken(String token, UserDetails userDetails) {
|
||||
var username = extractUsernameFromRefreshToken(token);
|
||||
return (username.equals(userDetails.getUsername()) && !isRefreshTokenExpired(token));
|
||||
}
|
||||
|
||||
private SecretKey getAccessSigningKey() {
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
|
||||
}
|
||||
|
||||
private SecretKey getRefreshSigningKey() {
|
||||
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(refreshSecret));
|
||||
}
|
||||
|
||||
private <T> T extractClaim(String token, Function<Claims, T> claimsResolver, SecretKey key) {
|
||||
final Claims claims = parseClaims(token, key);
|
||||
return claimsResolver.apply(claims);
|
||||
}
|
||||
|
||||
private Claims parseClaims(String token, SecretKey key) {
|
||||
return Jwts.parser().verifyWith(key).build().parseSignedClaims(token).getPayload();
|
||||
}
|
||||
|
||||
private Boolean isTokenExpired(String token) {
|
||||
return extractExpiration(token, getAccessSigningKey()).before(new Date());
|
||||
}
|
||||
|
||||
private Boolean isRefreshTokenExpired(String token) {
|
||||
return extractExpiration(token, getRefreshSigningKey()).before(new Date());
|
||||
}
|
||||
|
||||
public Date extractExpiration(String token, SecretKey key) {
|
||||
return extractClaim(token, Claims::getExpiration, key);
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,10 +44,11 @@ minio:
|
||||
secretKey: ${MINIO_PASS}
|
||||
bucket: mangamochi
|
||||
|
||||
# JWT Configuration
|
||||
jwt:
|
||||
secret: mySecretKeymySecretKeymySecretKeymySecretKeymySecretKeymySecretKey
|
||||
expiration: 86400000 # 24 hours in milliseconds
|
||||
secret: /JcSdxjeyeuMGoK5GD9w7OfqK/j+nvHR7uVUY12pNis=
|
||||
expiration: 3600000
|
||||
refresh-secret: MIV9ctIwrImmrZBjh9QueNEcDOLLVv9Rephii+0DKbk=
|
||||
refresh-expiration: 2629746000
|
||||
|
||||
manga-matcher:
|
||||
endpoint: ${MANGAMATCHER_ENDPOINT}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user