PasswordChangeRequiredFilter.java
package com.taxonomy.security.config;
import com.taxonomy.security.service.PasswordChangeService;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Profile;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
/**
* Restricts authenticated local users to password replacement until their
* bootstrap or administrator-assigned password has been changed.
*/
@Component
@Profile("!keycloak")
public class PasswordChangeRequiredFilter extends OncePerRequestFilter {
private static final String CHANGE_PASSWORD_PATH = "/change-password";
private static final String CHANGE_PASSWORD_API_PATH =
"/api/account/change-password";
private final PasswordChangeService passwordChangeService;
private final boolean enforcementEnabled;
public PasswordChangeRequiredFilter(
PasswordChangeService passwordChangeService,
@Value("${taxonomy.security.require-password-change:false}")
boolean enforcementEnabled) {
this.passwordChangeService = passwordChangeService;
this.enforcementEnabled = enforcementEnabled;
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !enforcementEnabled || isExemptPath(applicationPath(request));
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext()
.getAuthentication();
if (authentication == null || !authentication.isAuthenticated()
|| "anonymousUser".equals(authentication.getPrincipal())
|| !passwordChangeService.isPasswordChangeRequired(
authentication.getName())) {
filterChain.doFilter(request, response);
return;
}
String path = applicationPath(request);
if (isApiPath(path)) {
writePasswordChangeRequired(
response,
externalPath(request, CHANGE_PASSWORD_API_PATH));
return;
}
response.sendRedirect(externalPath(request, CHANGE_PASSWORD_PATH));
}
/** Removes the servlet context path before matching application routes. */
static String applicationPath(HttpServletRequest request) {
String requestUri = request.getRequestURI();
if (requestUri == null || requestUri.isBlank()) {
return "/";
}
String contextPath = contextPrefix(request);
if (contextPath.isEmpty()) {
return requestUri;
}
if (requestUri.equals(contextPath)) {
return "/";
}
if (requestUri.startsWith(contextPath + "/")) {
return requestUri.substring(contextPath.length());
}
return requestUri;
}
static boolean isExemptPath(String path) {
return path.equals("/login")
|| path.startsWith("/login/")
|| path.equals("/logout")
|| path.equals(CHANGE_PASSWORD_PATH)
|| path.equals(CHANGE_PASSWORD_API_PATH)
|| path.equals("/error")
|| path.startsWith("/error/")
|| path.startsWith("/css/")
|| path.startsWith("/js/")
|| path.startsWith("/images/")
|| path.startsWith("/webjars/");
}
private static boolean isApiPath(String path) {
return path.equals("/api") || path.startsWith("/api/");
}
private static String externalPath(
HttpServletRequest request,
String applicationPath) {
return contextPrefix(request) + applicationPath;
}
private static String contextPrefix(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath == null
|| contextPath.isBlank()
|| "/".equals(contextPath)
? "" : contextPath;
}
private static void writePasswordChangeRequired(
HttpServletResponse response,
String changePasswordEndpoint) throws IOException {
response.setStatus(428);
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store");
response.getWriter().write(
"{\"error\":\"PASSWORD_CHANGE_REQUIRED\","
+ "\"message\":\"Change the temporary password before using the API\","
+ "\"changePasswordEndpoint\":\""
+ changePasswordEndpoint + "\"}");
}
}