@@ -19,6 +19,10 @@ import (
1919// errRepoNotAllowed is returned when a valid token doesn't have access to the requested repository.
2020var errRepoNotAllowed = errors .New ("repository not allowed by token" )
2121
22+ // errInvalidTokenType is returned when a token is valid JWT but carries claims
23+ // reserved for registry-scoped builder tokens.
24+ var errInvalidTokenType = errors .New ("invalid token type" )
25+
2226type contextKey string
2327
2428const userIDKey contextKey = "user_id"
@@ -77,55 +81,17 @@ func OapiAuthenticationFunc(jwtSecret string) openapi3filter.AuthenticationFunc
7781 return fmt .Errorf ("invalid authorization header format" )
7882 }
7983
80- // Parse and validate JWT
81- claims := jwt.MapClaims {}
82- parsedToken , err := jwt .ParseWithClaims (token , claims , func (token * jwt.Token ) (interface {}, error ) {
83- // Validate signing method
84- if _ , ok := token .Method .(* jwt.SigningMethodHMAC ); ! ok {
85- return nil , fmt .Errorf ("unexpected signing method: %v" , token .Header ["alg" ])
86- }
87- return []byte (jwtSecret ), nil
88- })
89-
84+ claims , err := validateUserToken (token , jwtSecret )
9085 if err != nil {
91- log .DebugContext (ctx , "failed to parse JWT" , "error" , err )
92- return fmt .Errorf ("invalid token" )
93- }
94-
95- if ! parsedToken .Valid {
96- log .DebugContext (ctx , "invalid JWT token" )
86+ log .DebugContext (ctx , "user token validation failed" , "error" , err )
87+ if errors .Is (err , errInvalidTokenType ) {
88+ return errInvalidTokenType
89+ }
9790 return fmt .Errorf ("invalid token" )
9891 }
9992
100- // Reject registry tokens - they should not be used for API authentication.
101- // Registry tokens have specific claims (repos, scope, build_id) that user tokens don't have.
102- if _ , hasRepos := claims ["repos" ]; hasRepos {
103- log .DebugContext (ctx , "rejected registry token used for API auth" )
104- return fmt .Errorf ("invalid token type" )
105- }
106- if _ , hasScope := claims ["scope" ]; hasScope {
107- log .DebugContext (ctx , "rejected registry token used for API auth" )
108- return fmt .Errorf ("invalid token type" )
109- }
110- if _ , hasBuildID := claims ["build_id" ]; hasBuildID {
111- log .DebugContext (ctx , "rejected registry token used for API auth" )
112- return fmt .Errorf ("invalid token type" )
113- }
114-
115- // Extract user ID from claims and add to context
116- var userID string
117- if sub , ok := claims ["sub" ].(string ); ok {
118- userID = sub
119- }
120-
121- // Update the context with user ID
122- newCtx := context .WithValue (ctx , userIDKey , userID )
123-
124- // Extract scoped permissions from the "permissions" claim.
125- // Tokens without this claim get full access (backward compatibility).
126- newCtx = extractPermissions (newCtx , claims )
127-
128- // Update the request with the new context
93+ // Update the request with the authenticated user context.
94+ newCtx := contextWithUserClaims (ctx , claims )
12995 * input .RequestValidationInput .Request = * input .RequestValidationInput .Request .WithContext (newCtx )
13096
13197 return nil
@@ -339,6 +305,55 @@ func validateRegistryToken(tokenString, jwtSecret, requestPath, method string) (
339305 return claims , nil
340306}
341307
308+ // validateUserToken validates a regular user JWT and rejects builder/registry tokens.
309+ func validateUserToken (tokenString , jwtSecret string ) (jwt.MapClaims , error ) {
310+ claims := jwt.MapClaims {}
311+ parsedToken , err := jwt .ParseWithClaims (tokenString , claims , func (token * jwt.Token ) (interface {}, error ) {
312+ if _ , ok := token .Method .(* jwt.SigningMethodHMAC ); ! ok {
313+ return nil , fmt .Errorf ("unexpected signing method: %v" , token .Header ["alg" ])
314+ }
315+ return []byte (jwtSecret ), nil
316+ })
317+
318+ if err != nil {
319+ return nil , fmt .Errorf ("parse token: %w" , err )
320+ }
321+
322+ if ! parsedToken .Valid {
323+ return nil , fmt .Errorf ("invalid token" )
324+ }
325+
326+ // Reject registry tokens - they should not be used for API authentication.
327+ // Registry tokens have specific claims that user tokens don't have.
328+ if _ , hasRepos := claims ["repos" ]; hasRepos {
329+ return nil , errInvalidTokenType
330+ }
331+ if _ , hasScope := claims ["scope" ]; hasScope {
332+ return nil , errInvalidTokenType
333+ }
334+ if _ , hasBuildID := claims ["build_id" ]; hasBuildID {
335+ return nil , errInvalidTokenType
336+ }
337+ // Also reject tokens with "builder-" prefix in subject as an extra safeguard.
338+ if sub , ok := claims ["sub" ].(string ); ok && strings .HasPrefix (sub , "builder-" ) {
339+ return nil , errInvalidTokenType
340+ }
341+
342+ return claims , nil
343+ }
344+
345+ func contextWithUserClaims (ctx context.Context , claims jwt.MapClaims ) context.Context {
346+ var userID string
347+ if sub , ok := claims ["sub" ].(string ); ok {
348+ userID = sub
349+ }
350+
351+ ctx = context .WithValue (ctx , userIDKey , userID )
352+
353+ // Tokens without a permissions claim keep full access for backward compatibility.
354+ return extractPermissions (ctx , claims )
355+ }
356+
342357// JwtAuth creates a chi middleware that validates JWT bearer tokens
343358func JwtAuth (jwtSecret string ) func (http.Handler ) http.Handler {
344359 return func (next http.Handler ) http.Handler {
@@ -397,6 +412,18 @@ func JwtAuth(jwtSecret string) func(http.Handler) http.Handler {
397412 fmt .Fprintf (w , `{"errors":[{"code":"UNAVAILABLE","message":"image not mirrored"}]}` )
398413 return
399414 }
415+
416+ // Fall back to regular user JWTs for direct registry access.
417+ userClaims , err := validateUserToken (token , jwtSecret )
418+ if err == nil {
419+ log .DebugContext (r .Context (), "user token validated for registry request" ,
420+ "auth_type" , authType ,
421+ "subject" , userClaims ["sub" ])
422+ ctx := contextWithUserClaims (r .Context (), userClaims )
423+ next .ServeHTTP (w , r .WithContext (ctx ))
424+ return
425+ }
426+ log .DebugContext (r .Context (), "user token validation failed for registry request" , "error" , err )
400427 } else {
401428 log .DebugContext (r .Context (), "failed to extract token" , "error" , err )
402429 }
@@ -429,68 +456,19 @@ func JwtAuth(jwtSecret string) func(http.Handler) http.Handler {
429456 return
430457 }
431458
432- // Parse and validate as regular user JWT
433- claims := jwt.MapClaims {}
434- parsedToken , err := jwt .ParseWithClaims (token , claims , func (token * jwt.Token ) (interface {}, error ) {
435- // Validate signing method
436- if _ , ok := token .Method .(* jwt.SigningMethodHMAC ); ! ok {
437- return nil , fmt .Errorf ("unexpected signing method: %v" , token .Header ["alg" ])
438- }
439- return []byte (jwtSecret ), nil
440- })
441-
459+ claims , err := validateUserToken (token , jwtSecret )
442460 if err != nil {
443- log .DebugContext (r .Context (), "failed to parse JWT" , "error" , err )
444- OapiErrorHandler (w , "invalid token" , http .StatusUnauthorized )
445- return
446- }
447-
448- if ! parsedToken .Valid {
449- log .DebugContext (r .Context (), "invalid JWT token" )
461+ log .DebugContext (r .Context (), "user token validation failed" , "error" , err )
462+ if errors .Is (err , errInvalidTokenType ) {
463+ OapiErrorHandler (w , errInvalidTokenType .Error (), http .StatusUnauthorized )
464+ return
465+ }
450466 OapiErrorHandler (w , "invalid token" , http .StatusUnauthorized )
451467 return
452468 }
453469
454- // Reject registry tokens - they should not be used for API authentication.
455- // Registry tokens have specific claims that user tokens don't have.
456- // This provides defense-in-depth even though BuildKit isolates build containers.
457- if _ , hasRepos := claims ["repos" ]; hasRepos {
458- log .DebugContext (r .Context (), "rejected registry token used for API auth" )
459- OapiErrorHandler (w , "invalid token type" , http .StatusUnauthorized )
460- return
461- }
462- if _ , hasScope := claims ["scope" ]; hasScope {
463- log .DebugContext (r .Context (), "rejected registry token used for API auth" )
464- OapiErrorHandler (w , "invalid token type" , http .StatusUnauthorized )
465- return
466- }
467- if _ , hasBuildID := claims ["build_id" ]; hasBuildID {
468- log .DebugContext (r .Context (), "rejected registry token used for API auth" )
469- OapiErrorHandler (w , "invalid token type" , http .StatusUnauthorized )
470- return
471- }
472- // Also reject tokens with "builder-" prefix in subject as an extra safeguard
473- if sub , ok := claims ["sub" ].(string ); ok && strings .HasPrefix (sub , "builder-" ) {
474- log .DebugContext (r .Context (), "rejected builder token used for API auth" , "sub" , sub )
475- OapiErrorHandler (w , "invalid token type" , http .StatusUnauthorized )
476- return
477- }
478-
479- // Extract user ID from claims and add to context
480- var userID string
481- if sub , ok := claims ["sub" ].(string ); ok {
482- userID = sub
483- }
484-
485- // Update the context with user ID
486- newCtx := context .WithValue (r .Context (), userIDKey , userID )
487-
488- // Extract scoped permissions from the "permissions" claim.
489- // Tokens without this claim get full access (backward compatibility).
490- newCtx = extractPermissions (newCtx , claims )
491-
492470 // Call next handler with updated context
493- next .ServeHTTP (w , r .WithContext (newCtx ))
471+ next .ServeHTTP (w , r .WithContext (contextWithUserClaims ( r . Context (), claims ) ))
494472 })
495473 }
496474}
0 commit comments