Skip to content

Commit 6132ea6

Browse files
committed
fix(auth): publish InteractiveAuthenticationSuccessEvent from authWithoutPassword()
authWithoutPassword() bypassed Spring Security's normal authentication flow and never published InteractiveAuthenticationSuccessEvent. This meant BaseAuthenticationListener never fired (session profiles not populated) and AuthenticationEventListener.onSuccess() never fired (brute-force login attempt counters not reset). - Publish InteractiveAuthenticationSuccessEvent after storing security context in session - Add test verifying event is published on successful auth - Add verify(never) assertions to error-path tests Fixes #260
1 parent 5ad6f28 commit 6132ea6

2 files changed

Lines changed: 71 additions & 0 deletions

File tree

src/main/java/com/digitalsanctuary/spring/user/service/UserService.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import org.springframework.beans.factory.annotation.Value;
1111
import org.springframework.context.ApplicationEventPublisher;
1212
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13+
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
1314
import org.springframework.security.core.Authentication;
1415
import org.springframework.security.core.GrantedAuthority;
1516
import org.springframework.security.core.context.SecurityContextHolder;
@@ -554,6 +555,12 @@ public void authWithoutPassword(User user) {
554555
// Store security context in session
555556
storeSecurityContextInSession();
556557

558+
// Publish authentication event for listeners (session profile, brute-force reset)
559+
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
560+
if (authentication != null) {
561+
eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authentication, this.getClass()));
562+
}
563+
557564
log.debug("UserService.authWithoutPassword: authenticated user: {}", user.getEmail());
558565
}
559566

src/test/java/com/digitalsanctuary/spring/user/service/UserServiceTest.java

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
import org.springframework.context.ApplicationEventPublisher;
2828
import org.springframework.security.core.GrantedAuthority;
2929
import org.springframework.security.core.authority.SimpleGrantedAuthority;
30+
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
31+
import org.springframework.security.core.Authentication;
3032
import org.springframework.security.core.context.SecurityContext;
3133
import org.springframework.security.core.context.SecurityContextHolder;
3234
import org.springframework.security.core.session.SessionInformation;
@@ -477,6 +479,18 @@ void authWithoutPassword_authenticatesValidUser() {
477479
SecurityContext securityContext = mock(SecurityContext.class);
478480
mockedSecurityHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext);
479481

482+
// Capture the authentication set on the context and return it on getAuthentication()
483+
ArgumentCaptor<Authentication> authCaptor = ArgumentCaptor.forClass(Authentication.class);
484+
485+
// Use thenAnswer to capture and store the authentication set
486+
final Authentication[] storedAuth = new Authentication[1];
487+
org.mockito.Mockito.doAnswer(invocation -> {
488+
storedAuth[0] = invocation.getArgument(0);
489+
return null;
490+
}).when(securityContext).setAuthentication(any());
491+
492+
when(securityContext.getAuthentication()).thenAnswer(invocation -> storedAuth[0]);
493+
480494
// When
481495
userService.authWithoutPassword(testUser);
482496

@@ -488,6 +502,53 @@ void authWithoutPassword_authenticatesValidUser() {
488502
}
489503
}
490504

505+
@Test
506+
@DisplayName("authWithoutPassword - publishes InteractiveAuthenticationSuccessEvent")
507+
void shouldPublishInteractiveAuthenticationSuccessEventWhenAuthSucceeds() {
508+
// Given
509+
DSUserDetails userDetails = new DSUserDetails(testUser);
510+
Collection<? extends GrantedAuthority> authorities = Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
511+
512+
when(dsUserDetailsService.loadUserByUsername(testUser.getEmail())).thenReturn(userDetails);
513+
when(authorityService.getAuthoritiesFromUser(testUser)).thenReturn((Collection) authorities);
514+
515+
HttpServletRequest mockRequest = mock(HttpServletRequest.class);
516+
HttpSession mockSession = mock(HttpSession.class);
517+
ServletRequestAttributes attrs = mock(ServletRequestAttributes.class);
518+
519+
when(attrs.getRequest()).thenReturn(mockRequest);
520+
when(mockRequest.getSession(true)).thenReturn(mockSession);
521+
522+
try (MockedStatic<RequestContextHolder> mockedHolder = mockStatic(RequestContextHolder.class);
523+
MockedStatic<SecurityContextHolder> mockedSecurityHolder = mockStatic(
524+
SecurityContextHolder.class)) {
525+
526+
mockedHolder.when(RequestContextHolder::getRequestAttributes).thenReturn(attrs);
527+
SecurityContext securityContext = mock(SecurityContext.class);
528+
mockedSecurityHolder.when(SecurityContextHolder::getContext).thenReturn(securityContext);
529+
530+
// Return the authentication that was set
531+
final Authentication[] storedAuth = new Authentication[1];
532+
org.mockito.Mockito.doAnswer(invocation -> {
533+
storedAuth[0] = invocation.getArgument(0);
534+
return null;
535+
}).when(securityContext).setAuthentication(any());
536+
when(securityContext.getAuthentication()).thenAnswer(invocation -> storedAuth[0]);
537+
538+
// When
539+
userService.authWithoutPassword(testUser);
540+
541+
// Then
542+
ArgumentCaptor<InteractiveAuthenticationSuccessEvent> eventCaptor =
543+
ArgumentCaptor.forClass(InteractiveAuthenticationSuccessEvent.class);
544+
verify(eventPublisher).publishEvent(eventCaptor.capture());
545+
546+
InteractiveAuthenticationSuccessEvent event = eventCaptor.getValue();
547+
assertThat(event).isNotNull();
548+
assertThat(event.getAuthentication().getPrincipal()).isEqualTo(userDetails);
549+
}
550+
}
551+
491552
@Test
492553
@DisplayName("authWithoutPassword - handles null user")
493554
void authWithoutPassword_handlesNullUser() {
@@ -497,6 +558,7 @@ void authWithoutPassword_handlesNullUser() {
497558
// Then
498559
verify(dsUserDetailsService, never()).loadUserByUsername(any());
499560
verify(authorityService, never()).getAuthoritiesFromUser(any());
561+
verify(eventPublisher, never()).publishEvent(any());
500562
}
501563

502564
@Test
@@ -511,6 +573,7 @@ void authWithoutPassword_handlesUserWithNullEmail() {
511573
// Then
512574
verify(dsUserDetailsService, never()).loadUserByUsername(any());
513575
verify(authorityService, never()).getAuthoritiesFromUser(any());
576+
verify(eventPublisher, never()).publishEvent(any());
514577
}
515578

516579
@Test
@@ -525,6 +588,7 @@ void authWithoutPassword_handlesUserNotFound() {
525588

526589
// Then
527590
verify(authorityService, never()).getAuthoritiesFromUser(any());
591+
verify(eventPublisher, never()).publishEvent(any());
528592
}
529593

530594
@Test

0 commit comments

Comments
 (0)