OAuth2LoginSuccessHandler.java
package com.smartsupplypro.inventory.security;
import java.io.IOException;
import java.net.URI;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import com.smartsupplypro.inventory.config.AppProperties;
import com.smartsupplypro.inventory.model.AppUser;
import com.smartsupplypro.inventory.model.Role;
import com.smartsupplypro.inventory.repository.AppUserRepository;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
/**
* Enterprise OAuth2 authentication success handler with automatic user provisioning.
*
* <p>Handles post-authentication user onboarding and secure frontend redirection
* after successful Google OAuth2 login. Creates local user accounts with default
* permissions and manages cross-origin redirect security.</p>
*
* <p><strong>Enterprise Features:</strong> Automatic user provisioning, concurrent
* login safety, configurable frontend redirects, and origin allowlist security.</p>
*/
@Component
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private static final org.slf4j.Logger log =
org.slf4j.LoggerFactory.getLogger(OAuth2LoginSuccessHandler.class);
/** Application properties for frontend URL configuration and environment settings. */
private final AppProperties props;
/** User repository for automatic user provisioning and duplicate detection. */
private final AppUserRepository userRepository;
/** Constructor with dependency injection for configuration and data access. */
public OAuth2LoginSuccessHandler(AppProperties props, AppUserRepository userRepository) {
this.props = props;
this.userRepository = userRepository;
}
/**
* Handles successful OAuth2 authentication with automatic user provisioning.
*
* <p>Extracts user credentials from OAuth2 token, creates local user account
* if needed, and redirects to configured frontend URL with origin allowlist security.</p>
*
* <p><strong>Enterprise Security:</strong> Prevents duplicate redirects, validates
* return URLs against allowlist, and handles concurrent login scenarios safely.</p>
*/
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication)
throws IOException, ServletException {
// Enterprise Security: Prevent duplicate redirects in concurrent authentication scenarios
if (request.getAttribute("OAUTH2_SUCCESS_REDIRECT_DONE") != null) {
log.debug("Success redirect already performed; skipping.");
return;
}
request.setAttribute("OAUTH2_SUCCESS_REDIRECT_DONE", Boolean.TRUE);
// Enterprise Identity: Extract user credentials from OAuth2 token for local provisioning
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) authentication;
String email = token.getPrincipal().getAttribute("email");
String name = token.getPrincipal().getAttribute("name");
if (email == null || name == null) {
throw new IllegalStateException("Email or name not provided by OAuth2 provider");
}
try {
// Enterprise Provisioning: Automatic user creation with default role assignment
userRepository.findById(email).orElseGet(() -> {
log.info("Enterprise OAuth2: Creating new user account: {}", email);
AppUser newUser = new AppUser(email, name);
newUser.setRole(Role.USER); // Enterprise default: USER role for OAuth2 users
newUser.setCreatedAt(LocalDateTime.now());
return userRepository.save(newUser);
});
} catch (DataIntegrityViolationException e) {
// Enterprise Concurrency: Handle race conditions in multi-instance deployments
log.warn("Enterprise OAuth2: Concurrent user creation resolved for: {}", email);
userRepository.findByEmail(email).orElseThrow(() ->
new IllegalStateException("User already exists but cannot be loaded."));
}
// Enterprise Redirect Security: Origin allowlist prevents open redirect attacks
List<String> allowed = List.of(
"http://localhost:5173", // Development environment
"https://localhost:5173", // Development HTTPS
props.getFrontend().getBaseUrl() // Production frontend from configuration
);
// Enterprise Configuration: Environment-specific post-login landing page
String target = props.getFrontend().getBaseUrl() + props.getFrontend().getLandingPath();
// Enterprise Return URL: Check for custom return destination with security validation
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie c : cookies) {
if ("SSP_RETURN".equals(c.getName())) {
String candidate = c.getValue();
if (candidate != null && allowed.contains(candidate)) {
target = candidate + "/auth"; // Enterprise routing: custom post-auth destination
} else if (candidate != null) {
log.warn("Enterprise OAuth2: Rejected non-allowlisted return URL: {}", candidate);
}
// Enterprise Cleanup: Remove single-use return URL cookie
Cookie clear = new Cookie("SSP_RETURN", "");
clear.setPath("/");
clear.setMaxAge(0);
clear.setSecure(isSecureOrForwardedHttps(request));
clear.setHttpOnly(false); // Frontend created it non-HttpOnly
addCookieWithSameSite(response, clear, "None");
break;
}
}
}
log.info("Enterprise OAuth2: Authentication success, redirecting to: {}", target);
setAlwaysUseDefaultTargetUrl(true);
setDefaultTargetUrl(URI.create(target).toString());
// Enterprise Flow: Single redirect execution via parent handler
super.onAuthenticationSuccess(request, response, authentication);
}
/**
* Enterprise Security: Detects HTTPS context including load balancer forwarding.
* Supports both direct HTTPS and X-Forwarded-Proto header detection.
*/
private static boolean isSecureOrForwardedHttps(HttpServletRequest request) {
if (request.isSecure()) return true;
String xfProto = request.getHeader("X-Forwarded-Proto");
return xfProto != null && xfProto.equalsIgnoreCase("https");
}
/**
* Enterprise Cookie Security: Adds SameSite attribute for cross-origin compatibility.
* Essential for OAuth2 flows between frontend and backend on different domains.
*/
private static void addCookieWithSameSite(HttpServletResponse response, Cookie cookie, String sameSite) {
StringBuilder sb = new StringBuilder();
sb.append(cookie.getName()).append('=').append(cookie.getValue() == null ? "" : cookie.getValue());
sb.append("; Path=").append(cookie.getPath() == null ? "/" : cookie.getPath());
if (cookie.getMaxAge() >= 0) sb.append("; Max-Age=").append(cookie.getMaxAge());
if (cookie.getSecure()) sb.append("; Secure");
if (cookie.isHttpOnly()) sb.append("; HttpOnly");
if (sameSite != null && !sameSite.isBlank()) sb.append("; SameSite=").append(sameSite);
response.addHeader("Set-Cookie", sb.toString());
}
}