mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-14 10:34:06 +02:00
fix: storage APIs 401'd valid Supabase sessions (principal type mismatch)
FileStorageService.requireAuthenticatedUser and FolderService.requireAuthenticatedUser authorize via 'principal instanceof User', but EnhancedJwtAuthenticationToken extends JwtAuthenticationToken whose principal is the decoded Jwt - so every /api/v1/storage/* request 401'd for JWT users AFTER Spring Security had already authenticated them. This persistent 401-with-valid-session was the trigger feeding the frontend login loop. Attach the filter-resolved local User as the token principal for full accounts (User implements UserDetails, matching the form-login convention every shared instanceof check expects). Anonymous sessions keep the raw Jwt principal, preserving their existing exclusions. All other principal consumers verified safe: AuthenticationUtils checks instanceof User first, extractSupabaseId/CreditController/Team SecurityExpressions switch on the authentication type, not the principal. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
e7bbbb4702
commit
d29059e6fb
+26
@@ -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<? extends GrantedAuthority> authorities,
|
||||
String email,
|
||||
String supabaseId) {
|
||||
this(jwt, authorities, email, supabaseId, null);
|
||||
}
|
||||
|
||||
public EnhancedJwtAuthenticationToken(
|
||||
Jwt jwt,
|
||||
Collection<? extends GrantedAuthority> 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() {
|
||||
|
||||
+9
-1
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user