diff --git a/app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java b/app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java index c67845319..679e285f0 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java +++ b/app/saas/src/main/java/stirling/software/saas/security/EnhancedJwtAuthenticationToken.java @@ -6,6 +6,8 @@ import org.springframework.security.core.GrantedAuthority; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; +import stirling.software.proprietary.security.model.User; + /** * JWT auth token that exposes the Supabase subject UUID and email alongside the standard claims, so * downstream code (audit, credit accounting) can avoid re-parsing the JWT every request. @@ -14,15 +16,39 @@ public class EnhancedJwtAuthenticationToken extends JwtAuthenticationToken { private final String supabaseId; private final String email; + private final User user; public EnhancedJwtAuthenticationToken( Jwt jwt, Collection authorities, String email, String supabaseId) { + this(jwt, authorities, email, supabaseId, null); + } + + public EnhancedJwtAuthenticationToken( + Jwt jwt, + Collection authorities, + String email, + String supabaseId, + User user) { super(jwt, authorities, email); this.email = email; this.supabaseId = supabaseId; + this.user = user; + } + + /** + * Shared proprietary/core code authorizes with {@code principal instanceof User} (e.g. {@code + * FileStorageService.requireAuthenticatedUser}, {@code FolderService}), which the default + * {@link JwtAuthenticationToken} principal — the decoded {@link Jwt} — can never satisfy, so + * every such endpoint 401'd valid Supabase sessions. When the filter resolved a local {@link + * User}, expose it as the principal so those checks behave exactly as under form login; fall + * back to the Jwt when no User was attached (anonymous sessions, converter-built tokens). + */ + @Override + public Object getPrincipal() { + return user != null ? user : super.getPrincipal(); } public String getSupabaseId() { diff --git a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java index 08cdfcf57..888a5773f 100644 --- a/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java +++ b/app/saas/src/main/java/stirling/software/saas/security/SupabaseAuthenticationFilter.java @@ -155,9 +155,17 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter { User user = getOrCreateUser(jwt); + // Attach the resolved User as the token principal for full accounts so shared + // `principal instanceof User` authorization (storage services, account data) + // works under JWT auth. Anonymous sessions keep the raw Jwt principal and stay + // locked out of those User-gated APIs, matching pre-existing behaviour. EnhancedJwtAuthenticationToken authToken = new EnhancedJwtAuthenticationToken( - jwt, user.getAuthorities(), user.getUsername(), supabaseId); + jwt, + user.getAuthorities(), + user.getUsername(), + supabaseId, + isAnonymous(jwt) ? null : user); SecurityContextHolder.getContext().setAuthentication(authToken); // Hot path: runs on every authenticated request (>10 per page on a typical SPA),