Skip to content

Commit 2ae85c2

Browse files
committed
backend: auth: Extract RefreshAndCacheNewToken from headlamp.go
1 parent 71afcc3 commit 2ae85c2

File tree

3 files changed

+137
-29
lines changed

3 files changed

+137
-29
lines changed

backend/cmd/headlamp.go

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -804,34 +804,6 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
804804
return r
805805
}
806806

807-
func refreshAndCacheNewToken(oidcAuthConfig *kubeconfig.OidcConfig,
808-
cache cache.Cache[interface{}],
809-
tokenType, token, issuerURL string,
810-
) (*oauth2.Token, error) {
811-
ctx := context.Background()
812-
ctx = auth.ConfigureTLSContext(ctx, oidcAuthConfig.SkipTLSVerify, oidcAuthConfig.CACert)
813-
814-
// get provider
815-
provider, err := oidc.NewProvider(ctx, issuerURL)
816-
if err != nil {
817-
return nil, fmt.Errorf("getting provider: %v", err)
818-
}
819-
// get refresh token
820-
newToken, err := auth.GetNewToken(
821-
oidcAuthConfig.ClientID,
822-
oidcAuthConfig.ClientSecret,
823-
cache,
824-
tokenType,
825-
token,
826-
provider.Endpoint().TokenURL,
827-
)
828-
if err != nil {
829-
return nil, fmt.Errorf("refreshing token: %v", err)
830-
}
831-
832-
return newToken, nil
833-
}
834-
835807
func (c *HeadlampConfig) refreshAndSetToken(oidcAuthConfig *kubeconfig.OidcConfig,
836808
cache cache.Cache[interface{}], token string,
837809
w http.ResponseWriter, r *http.Request, cluster string, span trace.Span, ctx context.Context,
@@ -847,7 +819,8 @@ func (c *HeadlampConfig) refreshAndSetToken(oidcAuthConfig *kubeconfig.OidcConfi
847819
idpIssuerURL = oidcAuthConfig.IdpIssuerURL
848820
}
849821

850-
newToken, err := refreshAndCacheNewToken(
822+
newToken, err := auth.RefreshAndCacheNewToken(
823+
ctx,
851824
oidcAuthConfig,
852825
cache,
853826
tokenType,

backend/pkg/auth/auth.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131

3232
"github.com/coreos/go-oidc/v3/oidc"
3333
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
34+
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
3435
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
3536
"golang.org/x/oauth2"
3637
)
@@ -240,3 +241,33 @@ func ConfigureTLSContext(ctx context.Context, skipTLSVerify *bool, caCert *strin
240241

241242
return ctx
242243
}
244+
245+
// RefreshAndCacheNewToken obtains a fresh OIDC token using the cached refresh token
246+
// and re-populates the cache so subsequent requests can reuse it. The provided ctx
247+
// controls cancellation and deadlines for all outbound requests during the refresh.
248+
func RefreshAndCacheNewToken(ctx context.Context, oidcAuthConfig *kubeconfig.OidcConfig,
249+
cache cache.Cache[interface{}],
250+
tokenType, token, issuerURL string,
251+
) (*oauth2.Token, error) {
252+
ctx = ConfigureTLSContext(ctx, oidcAuthConfig.SkipTLSVerify, oidcAuthConfig.CACert)
253+
254+
// get provider
255+
provider, err := oidc.NewProvider(ctx, issuerURL)
256+
if err != nil {
257+
return nil, fmt.Errorf("getting provider: %w", err)
258+
}
259+
// get refresh token
260+
newToken, err := GetNewToken(
261+
oidcAuthConfig.ClientID,
262+
oidcAuthConfig.ClientSecret,
263+
cache,
264+
tokenType,
265+
token,
266+
provider.Endpoint().TokenURL,
267+
)
268+
if err != nil {
269+
return nil, fmt.Errorf("refreshing token: %w", err)
270+
}
271+
272+
return newToken, nil
273+
}

backend/pkg/auth/auth_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
565605
var 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.
693797
func TestConfigureTLSContext_NoConfig(t *testing.T) {
694798
baseCtx := context.Background()

0 commit comments

Comments
 (0)