mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 11:23:10 +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.jwt.Jwt;
|
||||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
|
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
|
* 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.
|
* 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 supabaseId;
|
||||||
private final String email;
|
private final String email;
|
||||||
|
private final User user;
|
||||||
|
|
||||||
public EnhancedJwtAuthenticationToken(
|
public EnhancedJwtAuthenticationToken(
|
||||||
Jwt jwt,
|
Jwt jwt,
|
||||||
Collection<? extends GrantedAuthority> authorities,
|
Collection<? extends GrantedAuthority> authorities,
|
||||||
String email,
|
String email,
|
||||||
String supabaseId) {
|
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);
|
super(jwt, authorities, email);
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.supabaseId = supabaseId;
|
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() {
|
public String getSupabaseId() {
|
||||||
|
|||||||
+9
-1
@@ -155,9 +155,17 @@ public class SupabaseAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
User user = getOrCreateUser(jwt);
|
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 =
|
EnhancedJwtAuthenticationToken authToken =
|
||||||
new EnhancedJwtAuthenticationToken(
|
new EnhancedJwtAuthenticationToken(
|
||||||
jwt, user.getAuthorities(), user.getUsername(), supabaseId);
|
jwt,
|
||||||
|
user.getAuthorities(),
|
||||||
|
user.getUsername(),
|
||||||
|
supabaseId,
|
||||||
|
isAnonymous(jwt) ? null : user);
|
||||||
SecurityContextHolder.getContext().setAuthentication(authToken);
|
SecurityContextHolder.getContext().setAuthentication(authToken);
|
||||||
|
|
||||||
// Hot path: runs on every authenticated request (>10 per page on a typical SPA),
|
// Hot path: runs on every authenticated request (>10 per page on a typical SPA),
|
||||||
|
|||||||
Reference in New Issue
Block a user