HmacAuthenticationFilter.java
package fr.avenirsesr.portfolio.api.infrastructure.adapter.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import fr.avenirsesr.portfolio.api.domain.exception.UserNotAuthorizedException;
import fr.avenirsesr.portfolio.api.domain.model.enums.ESecurityKeys;
import fr.avenirsesr.portfolio.api.infrastructure.adapter.model.UserPayload;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Arrays;
import java.util.Base64;
import java.util.List;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import lombok.NonNull;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
@Slf4j
@Component
public class HmacAuthenticationFilter extends OncePerRequestFilter {
@Value("${security.permit-all-paths}")
private String permitAllPathsString;
private List<String> permitAllPathsList;
public HmacAuthenticationFilter() {}
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain)
throws ServletException, IOException {
String signature = request.getHeader("X-Context-Signature");
String secretKey = ESecurityKeys.getSecretByKey(request.getHeader("X-Context-Kid"));
String payload = request.getHeader("X-Signed-Context");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
UserPayload userPayload = objectMapper.readValue(payload, UserPayload.class);
if (!payloadIsValid(userPayload)) {
UserNotAuthorizedException exception = new UserNotAuthorizedException();
log.error("Invalid HMAC authentication attempt. Payload is expired or invalid. {}", payload);
throw exception;
}
if (signature != null && verifySignature(payload, signature, secretKey)) {
Authentication auth = new HmacAuthenticationToken(userPayload.getSub());
SecurityContextHolder.getContext().setAuthentication(auth);
filterChain.doFilter(request, response);
} else {
UserNotAuthorizedException exception = new UserNotAuthorizedException();
log.error("Invalid HMAC authentication attempt.{}", String.valueOf(exception));
throw exception;
}
}
@Override
protected boolean shouldNotFilter(@NonNull HttpServletRequest request) throws ServletException {
if (permitAllPathsList == null) {
permitAllPathsList =
Arrays.stream(permitAllPathsString.split(","))
.map(path -> path.trim().replace("/**", ""))
.toList();
}
String path = request.getRequestURI();
return permitAllPathsList.stream().anyMatch(path::startsWith);
}
private boolean payloadIsValid(UserPayload payload) {
return payload != null && payload.getExp().isAfter(Instant.now());
}
private boolean verifySignature(String payload, String signature, String secretKey) {
try {
Mac sha256Hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKeySpec =
new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
sha256Hmac.init(secretKeySpec);
byte[] signedBytes = sha256Hmac.doFinal(payload.getBytes(StandardCharsets.UTF_8));
String computedSignature = Base64.getEncoder().encodeToString(signedBytes);
return computedSignature.equals(signature);
} catch (Exception e) {
return false;
}
}
}