From 90bda6b4b4c60c0f1d7a73b96c6131650b25aa00 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+frooodle@users.noreply.github.com> Date: Wed, 10 Jun 2026 14:37:12 +0100 Subject: [PATCH] fix: User principal was discarded by the resource server re-authentication The saas chain authenticates bearer requests twice: SupabaseAuthenticationFilter builds an EnhancedJwtAuthenticationToken with the resolved User principal, but BearerTokenAuthenticationFilter (oauth2ResourceServer) then re-authenticates the same token through the static toAuthentication converter and overwrites the SecurityContext with a token whose principal is the raw Jwt - so storage endpoints kept returning 401 "Unsupported user principal" despite the principal fix. Carry the User across in the converter: when the context already holds an EnhancedJwtAuthenticationToken for the same subject with a User principal, attach that User to the converter-built token. No extra DB lookups; anonymous sessions and API-key auth unchanged. Covered by new unit tests (carry, no-context, subject mismatch). --- .../saas/security/SupabaseSecurityConfig.java | 15 +++++- .../security/SupabaseSecurityConfigTest.java | 49 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java b/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java index feaa83a39..2c44a15c9 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java +++ b/app/saas/src/main/java/stirling/software/saas/security/SupabaseSecurityConfig.java @@ -23,8 +23,10 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2Error; import org.springframework.security.oauth2.core.OAuth2TokenValidator; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; @@ -44,6 +46,7 @@ import lombok.extern.slf4j.Slf4j; import stirling.software.common.model.ApplicationProperties; import stirling.software.common.util.RequestUriUtils; +import stirling.software.proprietary.security.model.User; import stirling.software.proprietary.security.service.TeamService; import stirling.software.proprietary.security.service.UserService; import stirling.software.saas.service.CreditService; @@ -333,6 +336,16 @@ public class SupabaseSecurityConfig { .map(GrantedAuthority::getAuthority) .collect(Collectors.joining(","))); } - return new EnhancedJwtAuthenticationToken(jwt, authorities, email, supabaseId); + // BearerTokenAuthenticationFilter overwrites the context SupabaseAuthenticationFilter + // built; carry its resolved User across so instanceof-User authorization keeps working. + User user = null; + Authentication existing = SecurityContextHolder.getContext().getAuthentication(); + if (existing instanceof EnhancedJwtAuthenticationToken enhanced + && supabaseId != null + && supabaseId.equals(enhanced.getSupabaseId()) + && enhanced.getPrincipal() instanceof User existingUser) { + user = existingUser; + } + return new EnhancedJwtAuthenticationToken(jwt, authorities, email, supabaseId, user); } } diff --git a/app/saas/src/test/java/stirling/software/saas/security/SupabaseSecurityConfigTest.java b/app/saas/src/test/java/stirling/software/saas/security/SupabaseSecurityConfigTest.java index 5c21b88a2..49779fce8 100644 --- a/app/saas/src/test/java/stirling/software/saas/security/SupabaseSecurityConfigTest.java +++ b/app/saas/src/test/java/stirling/software/saas/security/SupabaseSecurityConfigTest.java @@ -9,17 +9,25 @@ import java.util.List; import java.util.Map; import java.util.UUID; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.core.OAuth2TokenValidatorResult; import org.springframework.security.oauth2.jwt.Jwt; +import stirling.software.proprietary.security.model.User; import stirling.software.saas.security.SupabaseSecurityConfig.SupabaseTokenValidator; /** Unit tests for the JWT-claim → Spring authorities mapping. */ class SupabaseSecurityConfigTest { + @AfterEach + void clearSecurityContext() { + SecurityContextHolder.clearContext(); + } + @Test void anonymousJwtGetsLimitedApiUserRole() { Jwt jwt = jwtWith(true, null, null, null, List.of()); @@ -75,6 +83,47 @@ class SupabaseSecurityConfigTest { .doesNotContain("ROLE_", "PERM_"); } + @Test + void carriesUserPrincipalFromContextBuiltByAuthFilter() { + Jwt jwt = jwtWith(false, "alice@example.com", "authenticated", null, List.of()); + User user = new User(); + SecurityContextHolder.getContext() + .setAuthentication( + new EnhancedJwtAuthenticationToken( + jwt, List.of(), "alice@example.com", jwt.getSubject(), user)); + + AbstractAuthenticationToken auth = SupabaseSecurityConfig.toAuthentication(jwt); + + assertThat(auth.getPrincipal()).isSameAs(user); + } + + @Test + void principalStaysJwtWithoutContextUser() { + Jwt jwt = jwtWith(false, "alice@example.com", "authenticated", null, List.of()); + + AbstractAuthenticationToken auth = SupabaseSecurityConfig.toAuthentication(jwt); + + assertThat(auth.getPrincipal()).isSameAs(jwt); + } + + @Test + void ignoresContextUserForDifferentSubject() { + Jwt jwt = jwtWith(false, "alice@example.com", "authenticated", null, List.of()); + Jwt other = jwtWith(false, "bob@example.com", "authenticated", null, List.of()); + SecurityContextHolder.getContext() + .setAuthentication( + new EnhancedJwtAuthenticationToken( + other, + List.of(), + "bob@example.com", + other.getSubject(), + new User())); + + AbstractAuthenticationToken auth = SupabaseSecurityConfig.toAuthentication(jwt); + + assertThat(auth.getPrincipal()).isSameAs(jwt); + } + private static Jwt jwtWith( boolean anonymous, String email,