@@ -33,6 +33,7 @@ import (
3333
3434 "github.com/kubernetes-sigs/headlamp/backend/pkg/auth"
3535 "github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
36+ "github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
3637 "github.com/stretchr/testify/assert"
3738 "github.com/stretchr/testify/require"
3839 "golang.org/x/oauth2"
@@ -562,6 +563,45 @@ func newTokenServerJSON(t *testing.T, status int, body any) *httptest.Server {
562563 return srv
563564}
564565
566+ func newOIDCProviderServer (t * testing.T , tokenHandler http.HandlerFunc ) * httptest.Server {
567+ t .Helper ()
568+
569+ mux := http .NewServeMux ()
570+ srv := httptest .NewServer (mux )
571+ t .Cleanup (srv .Close )
572+
573+ mux .HandleFunc ("/.well-known/openid-configuration" , func (w http.ResponseWriter , r * http.Request ) {
574+ w .Header ().Set ("Content-Type" , "application/json" )
575+
576+ cfg := map [string ]any {
577+ "issuer" : srv .URL ,
578+ "token_endpoint" : srv .URL + "/token" ,
579+ "jwks_uri" : srv .URL + "/jwks" ,
580+ }
581+ if err := json .NewEncoder (w ).Encode (cfg ); err != nil {
582+ t .Fatalf ("encode discovery: %v" , err )
583+ }
584+ })
585+
586+ mux .HandleFunc ("/jwks" , func (w http.ResponseWriter , _ * http.Request ) {
587+ w .Header ().Set ("Content-Type" , "application/json" )
588+
589+ if _ , err := w .Write ([]byte (`{"keys":[]}` )); err != nil {
590+ t .Fatalf ("write jwks: %v" , err )
591+ }
592+ })
593+
594+ if tokenHandler != nil {
595+ mux .HandleFunc ("/token" , tokenHandler )
596+ } else {
597+ mux .HandleFunc ("/token" , func (w http.ResponseWriter , _ * http.Request ) {
598+ w .WriteHeader (http .StatusInternalServerError )
599+ })
600+ }
601+
602+ return srv
603+ }
604+
565605var oauthSuccessBody = map [string ]any {
566606 "access_token" : "AT" ,
567607 "token_type" : "Bearer" ,
@@ -689,6 +729,70 @@ func TestGetNewToken_CacheUpdateErrors(t *testing.T) {
689729 }
690730}
691731
732+ func TestRefreshAndCacheNewToken_Success (t * testing.T ) {
733+ const (
734+ oldToken = "OLD"
735+ oldKey = "oidc-token-" + oldToken
736+ )
737+
738+ fc := & fakeCache {store : map [string ]interface {}{oldKey : "REFRESH_OLD" }}
739+ srv := newOIDCProviderServer (t , func (w http.ResponseWriter , r * http.Request ) {
740+ require .NoError (t , r .ParseForm ())
741+ require .Equal (t , "refresh_token" , r .PostForm .Get ("grant_type" ))
742+ require .Equal (t , "REFRESH_OLD" , r .PostForm .Get ("refresh_token" ))
743+
744+ authHeader := r .Header .Get ("Authorization" )
745+ require .True (t , strings .HasPrefix (authHeader , "Basic " ), "expected Basic Authorization header" )
746+
747+ w .Header ().Set ("Content-Type" , "application/json" )
748+ require .NoError (t , json .NewEncoder (w ).Encode (oauthSuccessBody ))
749+ })
750+
751+ config := & kubeconfig.OidcConfig {ClientID : "cid" , ClientSecret : "secret" }
752+ tok , err := auth .RefreshAndCacheNewToken (context .Background (), config , fc , "id_token" , oldToken , srv .URL )
753+ require .NoError (t , err )
754+ assert .NotNil (t , tok )
755+ assert .Equal (t , refreshNew , tok .RefreshToken )
756+
757+ require .Len (t , fc .setCalls , 1 )
758+ assert .Equal (t , "oidc-token-NEW" , fc .setCalls [0 ].key )
759+ assert .Equal (t , refreshNew , fc .setCalls [0 ].val )
760+
761+ require .Len (t , fc .setWithTTLCalls , 1 )
762+ assert .Equal (t , oldKey , fc .setWithTTLCalls [0 ].key )
763+ assert .Equal (t , "REFRESH_OLD" , fc .setWithTTLCalls [0 ].val )
764+ assert .Equal (t , 10 * time .Second , fc .setWithTTLCalls [0 ].ttl )
765+ }
766+
767+ func TestRefreshAndCacheNewToken_ProviderError (t * testing.T ) {
768+ config := & kubeconfig.OidcConfig {ClientID : "cid" , ClientSecret : "secret" }
769+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
770+ http .Error (w , "no discovery" , http .StatusInternalServerError )
771+ }))
772+ t .Cleanup (srv .Close )
773+
774+ _ , err := auth .RefreshAndCacheNewToken (context .Background (), config , & fakeCache {}, "id_token" , "OLD" , srv .URL )
775+ require .Error (t , err )
776+ assert .Contains (t , err .Error (), "getting provider" )
777+ }
778+
779+ func TestRefreshAndCacheNewToken_TokenError (t * testing.T ) {
780+ const oldToken = "OLD"
781+ fc := & fakeCache {store : map [string ]interface {}{"oidc-token-" + oldToken : "REFRESH_OLD" }}
782+ srv := newOIDCProviderServer (t , func (w http.ResponseWriter , _ * http.Request ) {
783+ w .Header ().Set ("Content-Type" , "application/json" )
784+ w .WriteHeader (http .StatusInternalServerError )
785+ _ = json .NewEncoder (w ).Encode (map [string ]any {"error" : "server_error" })
786+ })
787+
788+ config := & kubeconfig.OidcConfig {ClientID : "cid" , ClientSecret : "secret" }
789+ _ , err := auth .RefreshAndCacheNewToken (context .Background (), config , fc , "id_token" , oldToken , srv .URL )
790+ require .Error (t , err )
791+ assert .Contains (t , err .Error (), "refreshing token" )
792+ assert .Len (t , fc .setCalls , 0 )
793+ assert .Len (t , fc .setWithTTLCalls , 0 )
794+ }
795+
692796// TestConfigureTLSContext_NoConfig tests when both skipTLSVerify and caCert are not set.
693797func TestConfigureTLSContext_NoConfig (t * testing.T ) {
694798 baseCtx := context .Background ()
0 commit comments