package com.example.security; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.authentication.dao.DaoAuthenticationProvider; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import java.util.List; /** * Wraps the standard username/password check with a downgrade step: if the authenticating user * has two-factor authentication enabled, the returned token carries only {@link #PRE_AUTH_ROLE} * instead of their real authorities. *

* This is the enforcement mechanism for 2FA — a session holding only that role satisfies no * {@code @PermitAll}/{@code @RolesAllowed} check on real application views, no matter what the * navigation layer does. {@link TwoFactorNavigationGuard} is the UX layer on top: it notices the * downgraded role and routes the user to the code-entry view. {@link TwoFactorService#verify} * plus {@link TwoFactorService#completeAuthentication} is what upgrades the session once the * correct code is supplied. */ @Component class TwoFactorAwareAuthenticationProvider implements AuthenticationProvider { // Deliberately not in the Role enum: that enum models roles persisted per-user, while this // is a transient authentication state no user is ever granted. static final String PRE_AUTH_ROLE = "PRE_AUTH_2FA"; static final String PRE_AUTH_AUTHORITY = "ROLE_" + PRE_AUTH_ROLE; private final DaoAuthenticationProvider delegate; private final TwoFactorService twoFactorService; TwoFactorAwareAuthenticationProvider( UserDetailsService userDetailsService, PasswordEncoder passwordEncoder, TwoFactorService twoFactorService) { delegate = new DaoAuthenticationProvider(userDetailsService); delegate.setPasswordEncoder(passwordEncoder); this.twoFactorService = twoFactorService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { var fullyAuthenticated = delegate.authenticate(authentication); if (fullyAuthenticated == null || !twoFactorService.isTwoFactorEnabled(fullyAuthenticated.getName())) { return fullyAuthenticated; } return UsernamePasswordAuthenticationToken.authenticated( fullyAuthenticated.getPrincipal(), fullyAuthenticated.getCredentials(), List.of(new SimpleGrantedAuthority(PRE_AUTH_AUTHORITY))); } @Override public boolean supports(Class authentication) { return delegate.supports(authentication); } }