@@ -14,6 +14,7 @@ import (
1414 "github.com/golang-jwt/jwt/v5"
1515 "github.com/stretchr/testify/require"
1616 "k8s.io/apimachinery/pkg/types"
17+ "k8s.io/client-go/rest"
1718
1819 "github.com/akuity/kargo/pkg/server/config"
1920 "github.com/akuity/kargo/pkg/server/dex"
@@ -52,6 +53,7 @@ func TestNewAuthInterceptor(t *testing.T) {
5253 require .NotNil (t , a .parseUnverifiedJWTFn )
5354 require .NotNil (t , a .verifyKargoIssuedTokenFn )
5455 require .NotNil (t , a .verifyIDPIssuedTokenFn )
56+ require .NotNil (t , a .verifyKubernetesTokenFn )
5557 require .NotNil (t , a .oidcExtractClaimsFn )
5658 require .NotNil (t , a .listServiceAccountsFn )
5759}
@@ -199,14 +201,10 @@ func TestAuthenticate(t *testing.T) {
199201 },
200202 },
201203 token : testToken ,
202- // We can't parse the token as a JWT, so we assume it could be an opaque
203- // bearer token for the k8s API server. We expect user info containing the
204- // raw token to be bound to the context.
205204 assertions : func (ctx context.Context , err error ) {
206- require .NoError (t , err )
207- u , ok := user .InfoFromContext (ctx )
208- require .True (t , ok )
209- require .Equal (t , testToken , u .BearerToken )
205+ require .Equal (t , "invalid token" , err .Error ())
206+ _ , ok := user .InfoFromContext (ctx )
207+ require .False (t , ok )
210208 },
211209 },
212210 "failure verifying Kargo-issued token" : {
@@ -354,7 +352,7 @@ func TestAuthenticate(t *testing.T) {
354352 require .Equal (t , testToken , u .BearerToken )
355353 },
356354 },
357- "unrecognized JWT" : {
355+ "unrecognized JWT recognized by Kubernetes " : {
358356 procedure : testProcedure ,
359357 authInterceptor : & authInterceptor {
360358 parseUnverifiedJWTFn : func (_ string , claims jwt.Claims ) (* jwt.Token , []string , error ) {
@@ -363,18 +361,44 @@ func TestAuthenticate(t *testing.T) {
363361 rc .Issuer = "unrecognized-issuer"
364362 return nil , nil , nil
365363 },
364+ verifyKubernetesTokenFn : func (context.Context , string ) error {
365+ return nil // Token is recognized by Kubernetes
366+ },
366367 },
367368 token : testToken ,
368- // We can't verify this token, so we assume it could be an an identity
369- // token from the k8s API server's identity provider. We expect user info
370- // containing the raw token to be bound to the context.
369+ // We can't verify this token, so we check if Kubernetes recognizes it.
370+ // In this case it does, so we expect user info containing the raw token
371+ // to be bound to the context.
371372 assertions : func (ctx context.Context , err error ) {
372373 require .NoError (t , err )
373374 u , ok := user .InfoFromContext (ctx )
374375 require .True (t , ok )
375376 require .Equal (t , testToken , u .BearerToken )
376377 },
377378 },
379+ "unrecognized JWT not recognized by Kubernetes" : {
380+ procedure : testProcedure ,
381+ authInterceptor : & authInterceptor {
382+ parseUnverifiedJWTFn : func (_ string , claims jwt.Claims ) (* jwt.Token , []string , error ) {
383+ rc , ok := claims .(* jwt.RegisteredClaims )
384+ require .True (t , ok )
385+ rc .Issuer = "unrecognized-issuer"
386+ return nil , nil , nil
387+ },
388+ verifyKubernetesTokenFn : func (context.Context , string ) error {
389+ return errors .New ("token not recognized" )
390+ },
391+ },
392+ token : testToken ,
393+ // We can't verify this token and Kubernetes doesn't recognize it either.
394+ // This should result in an authentication error.
395+ assertions : func (ctx context.Context , err error ) {
396+ require .Error (t , err )
397+ require .Equal (t , "invalid token" , err .Error ())
398+ _ , ok := user .InfoFromContext (ctx )
399+ require .False (t , ok )
400+ },
401+ },
378402 }
379403 for name , ts := range testSets {
380404 t .Run (name , func (t * testing.T ) {
@@ -594,3 +618,46 @@ func TestVerifyKargoIssuedToken(t *testing.T) {
594618 })
595619 }
596620}
621+
622+ func TestVerifyKubernetesToken (t * testing.T ) {
623+ const testToken = "test-bearer-token"
624+
625+ testCases := []struct {
626+ name string
627+ mockK8sAPIHandler http.HandlerFunc
628+ assertions func (t * testing.T , err error )
629+ }{
630+ {
631+ name : "Kubernetes API returns 200" ,
632+ mockK8sAPIHandler : func (w http.ResponseWriter , r * http.Request ) {
633+ // Verify the token was passed correctly
634+ require .Equal (t , "Bearer " + testToken , r .Header .Get ("Authorization" ))
635+ require .Equal (t , "/api" , r .URL .Path )
636+ w .WriteHeader (http .StatusOK )
637+ },
638+ assertions : func (t * testing.T , err error ) {
639+ require .NoError (t , err )
640+ },
641+ },
642+ {
643+ name : "Kubernetes API returns non-200" ,
644+ mockK8sAPIHandler : func (w http.ResponseWriter , _ * http.Request ) {
645+ w .WriteHeader (http .StatusUnauthorized )
646+ },
647+ assertions : func (t * testing.T , err error ) {
648+ require .ErrorContains (t , err , "unexpected response from Kubernetes API server: 401" )
649+ },
650+ },
651+ }
652+ for _ , testCase := range testCases {
653+ t .Run (testCase .name , func (t * testing.T ) {
654+ srv := httptest .NewServer (testCase .mockK8sAPIHandler )
655+ t .Cleanup (srv .Close )
656+ authenticator := & authInterceptor {
657+ cfg : config.ServerConfig {RestConfig : & rest.Config {Host : srv .URL }},
658+ }
659+ err := authenticator .verifyKubernetesToken (t .Context (), testToken )
660+ testCase .assertions (t , err )
661+ })
662+ }
663+ }
0 commit comments