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:
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user