66import de .tum .cit .aet .hephaestus .agent .job .AgentJobStatus ;
77import de .tum .cit .aet .hephaestus .agent .usage .LlmPriceSnapshot ;
88import de .tum .cit .aet .hephaestus .agent .usage .LlmUsageSourceType ;
9+ import de .tum .cit .aet .hephaestus .core .runtime .hub .auth .JobJwt ;
10+ import de .tum .cit .aet .hephaestus .core .runtime .hub .auth .WorkerJwtInvalidException ;
11+ import de .tum .cit .aet .hephaestus .core .runtime .hub .auth .WorkerJwtVerifier ;
912import jakarta .servlet .FilterChain ;
1013import jakarta .servlet .ServletException ;
1114import jakarta .servlet .http .HttpServletRequest ;
1417import java .math .BigDecimal ;
1518import java .net .InetAddress ;
1619import java .net .UnknownHostException ;
17- import java .security .MessageDigest ;
1820import java .util .Optional ;
1921import java .util .regex .Pattern ;
2022import org .jspecify .annotations .Nullable ;
@@ -36,20 +38,21 @@ public class JobTokenAuthenticationFilter extends OncePerRequestFilter {
3638
3739 private static final Logger log = LoggerFactory .getLogger (JobTokenAuthenticationFilter .class );
3840
39- /** Base64-URL characters (no padding). */
40- private static final Pattern BASE64_URL_PATTERN = Pattern .compile ("^[A-Za-z0-9_-]+$" );
41-
4241 private static final String BEARER_PREFIX = "Bearer " ;
42+ private static final String LLM_PROXY_SCOPE = "llm_proxy" ;
4343
4444 private final AgentJobRepository agentJobRepository ;
45+ private final WorkerJwtVerifier jwtVerifier ;
4546 private final MentorProxyCredentialRegistry mentorRegistry ;
4647 private final ObjectMapper objectMapper ;
4748
4849 JobTokenAuthenticationFilter (
4950 AgentJobRepository agentJobRepository ,
51+ WorkerJwtVerifier jwtVerifier ,
5052 MentorProxyCredentialRegistry mentorRegistry ,
5153 ObjectMapper objectMapper ) {
5254 this .agentJobRepository = agentJobRepository ;
55+ this .jwtVerifier = jwtVerifier ;
5356 this .mentorRegistry = mentorRegistry ;
5457 this .objectMapper = objectMapper ;
5558 }
@@ -69,12 +72,25 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
6972 return ;
7073 }
7174
72- if (!BASE64_URL_PATTERN .matcher (token ).matches ()) {
73- response .sendError (HttpServletResponse .SC_UNAUTHORIZED , "Invalid token format" );
74- return ;
75+ Optional <ProxyRouting > routing = mentorRegistry .validate (token );
76+ if (routing .isEmpty ()) {
77+ JobJwt jwt ;
78+ try {
79+ if (!(jwtVerifier .verify (token ) instanceof JobJwt jobJwt )) {
80+ response .sendError (HttpServletResponse .SC_UNAUTHORIZED , "Invalid or expired token" );
81+ return ;
82+ }
83+ jwt = jobJwt ;
84+ } catch (WorkerJwtInvalidException e ) {
85+ response .sendError (HttpServletResponse .SC_UNAUTHORIZED , "Invalid or expired token" );
86+ return ;
87+ }
88+ if (!jwt .scopes ().contains (LLM_PROXY_SCOPE )) {
89+ response .sendError (HttpServletResponse .SC_FORBIDDEN , "Insufficient token scope" );
90+ return ;
91+ }
92+ routing = resolveJobRouting (jwt );
7593 }
76-
77- Optional <ProxyRouting > routing = resolveJobRouting (token ).or (() -> mentorRegistry .validate (token ));
7894 if (routing .isEmpty ()) {
7995 response .sendError (HttpServletResponse .SC_UNAUTHORIZED , "Invalid or expired token" );
8096 return ;
@@ -88,20 +104,15 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
88104 }
89105 }
90106
91- /**
92- * Look up an {@code AgentJob} by token and translate its frozen {@link ConfigSnapshot} into routing.
93- * The attempt's identity and spend-so-far are read here because the row and snapshot already loaded
94- * carry both, so the budget gate costs no extra query.
95- */
96- private Optional <ProxyRouting > resolveJobRouting (String token ) {
97- String hash = AgentJob .computeTokenHash (token );
98- Optional <AgentJob > optionalJob = agentJobRepository .findByJobTokenHashAndStatus (hash , AgentJobStatus .RUNNING );
107+ private Optional <ProxyRouting > resolveJobRouting (JobJwt jwt ) {
108+ Optional <AgentJob > optionalJob = agentJobRepository .findByIdWithWorkspace (jwt .jobId ());
99109 if (optionalJob .isEmpty ()) {
100110 return Optional .empty ();
101111 }
102112 AgentJob job = optionalJob .get ();
103- if (!MessageDigest .isEqual (token .getBytes (), job .getJobToken ().getBytes ())) {
104- log .warn ("Token hash matched but constant-time comparison failed — possible collision" );
113+ if (job .getStatus () != AgentJobStatus .RUNNING
114+ || !job .getWorkspace ().getId ().equals (jwt .workspaceId ())
115+ || job .getRetryCount () != jwt .attempt ()) {
105116 return Optional .empty ();
106117 }
107118 if (job .getConfigSnapshot () == null ) {
0 commit comments