fix: implement refresh token functionality and update JWT configuration
This commit is contained in:
parent
55f1710eac
commit
0ec616a45b
@ -38,8 +38,7 @@ public class SecurityConfig {
|
|||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public DaoAuthenticationProvider authenticationProvider() {
|
public DaoAuthenticationProvider authenticationProvider() {
|
||||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
var authProvider = new DaoAuthenticationProvider(userDetailsService);
|
||||||
authProvider.setUserDetailsService(userDetailsService);
|
|
||||||
authProvider.setPasswordEncoder(passwordEncoder());
|
authProvider.setPasswordEncoder(passwordEncoder());
|
||||||
return authProvider;
|
return authProvider;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,6 @@
|
|||||||
package com.magamochi.mangamochi.controller;
|
package com.magamochi.mangamochi.controller;
|
||||||
|
|
||||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
import com.magamochi.mangamochi.model.dto.*;
|
||||||
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.service.UserService;
|
import com.magamochi.mangamochi.service.UserService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@ -27,6 +24,17 @@ public class AuthenticationController {
|
|||||||
return DefaultResponseDTO.ok(userService.authenticate(authenticationRequestDTO));
|
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(
|
@Operation(
|
||||||
summary = "Register user",
|
summary = "Register user",
|
||||||
description = "Register a new user.",
|
description = "Register a new user.",
|
||||||
|
|||||||
@ -5,7 +5,8 @@ import jakarta.validation.constraints.NotNull;
|
|||||||
|
|
||||||
public record AuthenticationResponseDTO(
|
public record AuthenticationResponseDTO(
|
||||||
@NotNull Long id,
|
@NotNull Long id,
|
||||||
@NotNull String token,
|
@NotNull String accessToken,
|
||||||
|
@NotNull String refreshToken,
|
||||||
@NotNull String email,
|
@NotNull String email,
|
||||||
@NotNull String name,
|
@NotNull String name,
|
||||||
@NotNull UserRole role) {}
|
@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;
|
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.service.CustomUserDetailsService;
|
||||||
import com.magamochi.mangamochi.util.JwtUtil;
|
import com.magamochi.mangamochi.util.JwtUtil;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
@ -8,9 +11,9 @@ import jakarta.servlet.http.HttpServletRequest;
|
|||||||
import jakarta.servlet.http.HttpServletResponse;
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.jetbrains.annotations.NotNull;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
|
||||||
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.web.filter.OncePerRequestFilter;
|
import org.springframework.web.filter.OncePerRequestFilter;
|
||||||
@ -18,40 +21,46 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
@Component
|
@Component
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JwtRequestFilter extends OncePerRequestFilter {
|
public class JwtRequestFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtUtil jwtUtil;
|
private final JwtUtil jwtUtil;
|
||||||
private final CustomUserDetailsService userDetailsService;
|
private final CustomUserDetailsService userDetailsService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(
|
protected void doFilterInternal(
|
||||||
HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain chain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
|
|
||||||
final String authorizationHeader = request.getHeader("Authorization");
|
final String authorizationHeader = request.getHeader("Authorization");
|
||||||
|
|
||||||
String username = null;
|
if (nonNull(authorizationHeader) && authorizationHeader.startsWith("Bearer ")) {
|
||||||
String jwt = null;
|
var jwt = authorizationHeader.substring(7);
|
||||||
|
|
||||||
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
|
|
||||||
jwt = authorizationHeader.substring(7);
|
|
||||||
try {
|
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) {
|
} 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);
|
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.exception.NotFoundException;
|
||||||
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
import com.magamochi.mangamochi.model.dto.AuthenticationRequestDTO;
|
||||||
import com.magamochi.mangamochi.model.dto.AuthenticationResponseDTO;
|
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.dto.RegistrationRequestDTO;
|
||||||
import com.magamochi.mangamochi.model.entity.User;
|
import com.magamochi.mangamochi.model.entity.User;
|
||||||
import com.magamochi.mangamochi.model.enumeration.UserRole;
|
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.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
import org.springframework.security.core.AuthenticationException;
|
|
||||||
import org.springframework.security.core.context.SecurityContextHolder;
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
@ -33,25 +33,47 @@ public class UserService {
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
|
||||||
public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
|
public AuthenticationResponseDTO authenticate(AuthenticationRequestDTO request) {
|
||||||
var token = new UsernamePasswordAuthenticationToken(request.email(), request.password());
|
authenticationManager.authenticate(
|
||||||
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
var userDetails = userDetailsService.loadUserByUsername(request.email());
|
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());
|
var user = findUserByEmailThrowIfNotFound(userDetails.getUsername());
|
||||||
|
|
||||||
return new AuthenticationResponseDTO(
|
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) {
|
public void register(RegistrationRequestDTO request) {
|
||||||
|
|||||||
@ -2,13 +2,13 @@ package com.magamochi.mangamochi.util;
|
|||||||
|
|
||||||
import io.jsonwebtoken.Claims;
|
import io.jsonwebtoken.Claims;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.SignatureAlgorithm;
|
import io.jsonwebtoken.io.Decoders;
|
||||||
import io.jsonwebtoken.security.Keys;
|
import io.jsonwebtoken.security.Keys;
|
||||||
import java.security.Key;
|
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
import javax.crypto.SecretKey;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.security.core.userdetails.UserDetails;
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
@ -18,51 +18,84 @@ public class JwtUtil {
|
|||||||
@Value("${jwt.secret}")
|
@Value("${jwt.secret}")
|
||||||
private String secret;
|
private String secret;
|
||||||
|
|
||||||
|
@Value("${jwt.refresh-secret}")
|
||||||
|
private String refreshSecret;
|
||||||
|
|
||||||
@Value("${jwt.expiration}")
|
@Value("${jwt.expiration}")
|
||||||
private Long expiration;
|
private Long expiration;
|
||||||
|
|
||||||
private Key getSigningKey() {
|
@Value("${jwt.refresh-expiration}")
|
||||||
return Keys.hmacShaKeyFor(secret.getBytes());
|
private Long refreshExpiration;
|
||||||
}
|
|
||||||
|
|
||||||
public String extractUsername(String token) {
|
public String extractUsername(String token) {
|
||||||
return extractClaim(token, Claims::getSubject);
|
return extractClaim(token, Claims::getSubject, getAccessSigningKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public Date extractExpiration(String token) {
|
public String extractUsernameFromRefreshToken(String token) {
|
||||||
return extractClaim(token, Claims::getExpiration);
|
return extractClaim(token, Claims::getSubject, getRefreshSigningKey());
|
||||||
}
|
}
|
||||||
|
|
||||||
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
|
public String generateAccessToken(UserDetails userDetails) {
|
||||||
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) {
|
|
||||||
Map<String, Object> claims = new HashMap<>();
|
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()
|
return Jwts.builder()
|
||||||
.setClaims(claims)
|
.claims(claims)
|
||||||
.setSubject(subject)
|
.subject(subject)
|
||||||
.setIssuedAt(new Date(System.currentTimeMillis()))
|
.issuedAt(now)
|
||||||
.setExpiration(new Date(System.currentTimeMillis() + expiration))
|
.expiration(expiry)
|
||||||
.signWith(getSigningKey(), SignatureAlgorithm.HS256)
|
.signWith(key)
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Boolean validateToken(String token, UserDetails userDetails) {
|
public Boolean validateAccessToken(String token, UserDetails userDetails) {
|
||||||
final String username = extractUsername(token);
|
var username = extractUsername(token);
|
||||||
return (username.equals(userDetails.getUsername()) && !isTokenExpired(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}
|
secretKey: ${MINIO_PASS}
|
||||||
bucket: mangamochi
|
bucket: mangamochi
|
||||||
|
|
||||||
# JWT Configuration
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: mySecretKeymySecretKeymySecretKeymySecretKeymySecretKeymySecretKey
|
secret: /JcSdxjeyeuMGoK5GD9w7OfqK/j+nvHR7uVUY12pNis=
|
||||||
expiration: 86400000 # 24 hours in milliseconds
|
expiration: 3600000
|
||||||
|
refresh-secret: MIV9ctIwrImmrZBjh9QueNEcDOLLVv9Rephii+0DKbk=
|
||||||
|
refresh-expiration: 2629746000
|
||||||
|
|
||||||
manga-matcher:
|
manga-matcher:
|
||||||
endpoint: ${MANGAMATCHER_ENDPOINT}
|
endpoint: ${MANGAMATCHER_ENDPOINT}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user