FormLoginAuthenticationSuccessHandler.java
/*
* Copyright (c) 2007-2026 MetaSolutions AB
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.entrystore.rest.springboot.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.entrystore.rest.springboot.filter.CacheControlFilter;
import org.entrystore.rest.springboot.filter.ReloadUserPropertiesFilter;
import org.entrystore.rest.springboot.model.api.ErrorResponse;
import org.entrystore.rest.springboot.service.auth.LoginAttemptService;
import org.entrystore.rest.springboot.model.auth.SessionInfo;
import org.entrystore.rest.springboot.util.ErrorResponseWriter;
import org.entrystore.rest.springboot.util.HttpUtil;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.concurrent.atomic.AtomicReference;
/**
* Completes a form login: session lifetime, the session info that /auth/tokens reports, and stale cookies. A login
* whose session info cannot be recorded fails with 500 and leaves no session, since later requests of the session
* rely on that info.
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class FormLoginAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final LoginAttemptService loginAttemptService;
private final AuthTokenCookies authTokenCookies;
private final ErrorResponseWriter errorResponseWriter;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth) throws IOException {
String username = request.getParameter("auth_username");
if (username != null) {
loginAttemptService.recordSuccess(username.toLowerCase());
}
authTokenCookies.applySessionLifetime(request, parseRequestedMaxAge(request.getParameter("auth_maxage")));
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getPrincipal() instanceof ESUserSessionDetails esUserDetails) {
try {
Instant now = Instant.now();
SessionInfo.SessionInfoBuilder sessionInfo = SessionInfo.builder()
.userName(username != null ? username.toLowerCase() : null)
.loginTime(LocalDateTime.ofInstant(now, ZoneId.systemDefault()))
.loginExpiration(LocalDateTime.ofInstant(authTokenCookies.expiry(request.getSession()), ZoneId.systemDefault()))
.lastAccessTime(LocalDateTime.ofInstant(now, ZoneId.systemDefault()))
.lastUsedIpAddress(request.getRemoteAddr())
.lastUsedUserAgent(request.getHeader("User-Agent"))
.loginTokenMaxAge(request.getSession().getMaxInactiveInterval());
esUserDetails.setSessionInfo(sessionInfo.build());
// the login has just loaded the user
request.getSession().setAttribute(ReloadUserPropertiesFilter.LAST_RELOAD_ATTRIBUTE,
new AtomicReference<>(now));
UsernamePasswordAuthenticationToken newAuth = new UsernamePasswordAuthenticationToken(esUserDetails, esUserDetails.getPassword(), esUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(newAuth);
} catch (RuntimeException e) {
log.error("Failed to record the session info of a login, ending the new session", e);
HttpUtil.clearAuthenticatedSession(request);
authTokenCookies.expireAfterFailedLogin(request, response);
errorResponseWriter.writeErrorResponseAsJson(response, ErrorResponse.builder()
.status(HttpStatus.INTERNAL_SERVER_ERROR.value())
.path(request.getRequestURI())
.error("Login failed.")
.build());
return;
}
}
authTokenCookies.expireStale(request, response);
// The login filter ends the chain before CacheControlFilter runs, and this response sets the session cookie.
CacheControlFilter.markSessionCookieResponse(response);
response.setStatus(HttpStatus.OK.value());
response.setContentType("text/html");
response.getWriter().write("Login successful.");
}
/**
* @return the client's {@code auth_maxage} in seconds, or null if absent or not a number
*/
private static Integer parseRequestedMaxAge(String maxAgeParam) {
if (StringUtils.isEmpty(maxAgeParam)) {
return null;
}
try {
return Integer.parseInt(maxAgeParam);
} catch (NumberFormatException e) {
log.info("Unable to parse as Integer the 'auth_maxage' parameter value of: '{}'", maxAgeParam);
return null;
}
}
}