feat: add tenant context resolution from Logto organization_id claim

TenantResolutionFilter extracts organization_id from Logto JWT and
resolves to local tenant via TenantService. ThreadLocal TenantContext
available throughout request lifecycle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
hsiegeln
2026-04-04 15:05:05 +02:00
parent 0d9c51843d
commit e58e2caf8e
3 changed files with 75 additions and 2 deletions

View File

@@ -10,6 +10,7 @@ import org.springframework.security.config.annotation.web.configuration.EnableWe
import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.server.resource.web.authentication.BearerTokenAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@@ -19,9 +20,11 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
public class SecurityConfig { public class SecurityConfig {
private final JwtAuthenticationFilter machineTokenFilter; private final JwtAuthenticationFilter machineTokenFilter;
private final TenantResolutionFilter tenantResolutionFilter;
public SecurityConfig(JwtAuthenticationFilter machineTokenFilter) { public SecurityConfig(JwtAuthenticationFilter machineTokenFilter, TenantResolutionFilter tenantResolutionFilter) {
this.machineTokenFilter = machineTokenFilter; this.machineTokenFilter = machineTokenFilter;
this.tenantResolutionFilter = tenantResolutionFilter;
} }
@Bean @Bean
@@ -50,7 +53,8 @@ public class SecurityConfig {
.anyRequest().authenticated() .anyRequest().authenticated()
) )
.oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {})) .oauth2ResourceServer(oauth2 -> oauth2.jwt(jwt -> {}))
.addFilterBefore(machineTokenFilter, UsernamePasswordAuthenticationFilter.class); .addFilterBefore(machineTokenFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterAfter(tenantResolutionFilter, BearerTokenAuthenticationFilter.class);
return http.build(); return http.build();
} }

View File

@@ -0,0 +1,22 @@
package net.siegeln.cameleer.saas.config;
import java.util.UUID;
public final class TenantContext {
private static final ThreadLocal<UUID> CURRENT_TENANT = new ThreadLocal<>();
private TenantContext() {}
public static UUID getTenantId() {
return CURRENT_TENANT.get();
}
public static void setTenantId(UUID tenantId) {
CURRENT_TENANT.set(tenantId);
}
public static void clear() {
CURRENT_TENANT.remove();
}
}

View File

@@ -0,0 +1,47 @@
package net.siegeln.cameleer.saas.config;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import net.siegeln.cameleer.saas.tenant.TenantService;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class TenantResolutionFilter extends OncePerRequestFilter {
private final TenantService tenantService;
public TenantResolutionFilter(TenantService tenantService) {
this.tenantService = tenantService;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
var authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication instanceof JwtAuthenticationToken jwtAuth) {
Jwt jwt = jwtAuth.getToken();
String orgId = jwt.getClaimAsString("organization_id");
if (orgId != null) {
tenantService.getByLogtoOrgId(orgId)
.ifPresent(tenant -> TenantContext.setTenantId(tenant.getId()));
}
}
filterChain.doFilter(request, response);
} finally {
TenantContext.clear();
}
}
}