Skip to content

Commit 7e34534

Browse files
committed
backend: auth: Extract RefreshAndSetToken from headlamp.go
1 parent ce95d2a commit 7e34534

File tree

3 files changed

+213
-44
lines changed

3 files changed

+213
-44
lines changed

backend/cmd/headlamp.go

Lines changed: 14 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -923,49 +923,6 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
923923
return r
924924
}
925925

926-
func (c *HeadlampConfig) refreshAndSetToken(oidcAuthConfig *kubeconfig.OidcConfig,
927-
cache cache.Cache[interface{}], token string,
928-
w http.ResponseWriter, r *http.Request, cluster string, span trace.Span, ctx context.Context,
929-
) {
930-
// The token type to use
931-
tokenType := "id_token"
932-
if c.oidcUseAccessToken {
933-
tokenType = "access_token"
934-
}
935-
936-
idpIssuerURL := c.oidcIdpIssuerURL
937-
if idpIssuerURL == "" {
938-
idpIssuerURL = oidcAuthConfig.IdpIssuerURL
939-
}
940-
941-
newToken, err := auth.RefreshAndCacheNewToken(
942-
ctx,
943-
oidcAuthConfig,
944-
cache,
945-
tokenType,
946-
token,
947-
idpIssuerURL,
948-
)
949-
if err != nil {
950-
logger.Log(logger.LevelError, map[string]string{"cluster": cluster},
951-
err, "failed to refresh token")
952-
c.telemetryHandler.RecordError(span, err, "Token refresh failed")
953-
c.telemetryHandler.RecordErrorCount(ctx, attribute.String("error", "token_refresh_failure"))
954-
} else if newToken != nil {
955-
var newTokenString string
956-
if c.oidcUseAccessToken {
957-
newTokenString = newToken.Extra("access_token").(string)
958-
} else {
959-
newTokenString = newToken.Extra("id_token").(string)
960-
}
961-
962-
// Set refreshed token in cookie
963-
auth.SetTokenCookie(w, r, cluster, newTokenString, c.BaseURL)
964-
965-
c.telemetryHandler.RecordEvent(span, "Token refreshed successfully")
966-
}
967-
}
968-
969926
func (c *HeadlampConfig) incrementRequestCounter(ctx context.Context) {
970927
if c.Metrics != nil {
971928
c.Metrics.RequestCounter.Add(ctx, 1,
@@ -1094,7 +1051,20 @@ func (c *HeadlampConfig) OIDCTokenRefreshMiddleware(next http.Handler) http.Hand
10941051
}
10951052

10961053
// refresh and cache new token
1097-
c.refreshAndSetToken(oidcAuthConfig, c.cache, token, w, r, cluster, span, ctx)
1054+
auth.RefreshAndSetToken(auth.RefreshAndSetTokenParams{
1055+
Ctx: ctx,
1056+
OIDCAuthConfig: oidcAuthConfig,
1057+
Cache: c.cache,
1058+
Token: token,
1059+
Cluster: cluster,
1060+
Span: span,
1061+
Writer: w,
1062+
Request: r,
1063+
TelemetryHandler: c.telemetryHandler,
1064+
OIDCUseAccessToken: c.oidcUseAccessToken,
1065+
OIDCIdpIssuerURL: c.oidcIdpIssuerURL,
1066+
BaseURL: c.BaseURL,
1067+
})
10981068

10991069
next.ServeHTTP(w, r)
11001070
c.telemetryHandler.RecordDuration(ctx, start,

backend/pkg/auth/auth.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ import (
3636
cfg "github.com/kubernetes-sigs/headlamp/backend/pkg/config"
3737
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
3838
"github.com/kubernetes-sigs/headlamp/backend/pkg/logger"
39+
"github.com/kubernetes-sigs/headlamp/backend/pkg/telemetry"
40+
"go.opentelemetry.io/otel/attribute"
41+
"go.opentelemetry.io/otel/trace"
3942
"golang.org/x/oauth2"
4043
)
4144

@@ -487,3 +490,62 @@ func marshalToString(val interface{}) (string, bool) {
487490

488491
return string(b), true
489492
}
493+
494+
// RefreshAndSetTokenParams groups the inputs required to refresh a token and
495+
// update the Headlamp auth cookie.
496+
type RefreshAndSetTokenParams struct {
497+
Ctx context.Context
498+
OIDCAuthConfig *kubeconfig.OidcConfig
499+
Cache cache.Cache[interface{}]
500+
Token string
501+
Cluster string
502+
Span trace.Span
503+
Writer http.ResponseWriter
504+
Request *http.Request
505+
TelemetryHandler *telemetry.RequestHandler
506+
OIDCUseAccessToken bool
507+
OIDCIdpIssuerURL string
508+
BaseURL string
509+
}
510+
511+
// RefreshAndSetToken refreshes an expiring token, updates the auth cookie,
512+
// and records telemetry based on the provided parameters.
513+
func RefreshAndSetToken(params RefreshAndSetTokenParams) {
514+
// The token type to use
515+
tokenType := "id_token"
516+
if params.OIDCUseAccessToken {
517+
tokenType = "access_token"
518+
}
519+
520+
idpIssuerURL := params.OIDCIdpIssuerURL
521+
if idpIssuerURL == "" {
522+
idpIssuerURL = params.OIDCAuthConfig.IdpIssuerURL
523+
}
524+
525+
newToken, err := RefreshAndCacheNewToken(
526+
params.Ctx,
527+
params.OIDCAuthConfig,
528+
params.Cache,
529+
tokenType,
530+
params.Token,
531+
idpIssuerURL,
532+
)
533+
if err != nil {
534+
logger.Log(logger.LevelError, map[string]string{"cluster": params.Cluster},
535+
err, "failed to refresh token")
536+
params.TelemetryHandler.RecordError(params.Span, err, "Token refresh failed")
537+
params.TelemetryHandler.RecordErrorCount(params.Ctx, attribute.String("error", "token_refresh_failure"))
538+
} else if newToken != nil {
539+
var newTokenString string
540+
if params.OIDCUseAccessToken {
541+
newTokenString = newToken.Extra("access_token").(string)
542+
} else {
543+
newTokenString = newToken.Extra("id_token").(string)
544+
}
545+
546+
// Set refreshed token in cookie
547+
SetTokenCookie(params.Writer, params.Request, params.Cluster, newTokenString, params.BaseURL)
548+
549+
params.TelemetryHandler.RecordEvent(params.Span, "Token refreshed successfully")
550+
}
551+
}

backend/pkg/auth/auth_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import (
3636
"github.com/kubernetes-sigs/headlamp/backend/pkg/auth"
3737
"github.com/kubernetes-sigs/headlamp/backend/pkg/cache"
3838
"github.com/kubernetes-sigs/headlamp/backend/pkg/kubeconfig"
39+
"github.com/kubernetes-sigs/headlamp/backend/pkg/telemetry"
3940
"github.com/stretchr/testify/assert"
4041
"github.com/stretchr/testify/require"
4142
"golang.org/x/oauth2"
@@ -604,6 +605,17 @@ func newOIDCProviderServer(t *testing.T, tokenHandler http.HandlerFunc) *httptes
604605
return srv
605606
}
606607

608+
func findAuthCookie(resp *http.Response, cluster string) (string, bool) {
609+
want := fmt.Sprintf("headlamp-auth-%s.0", auth.SanitizeClusterName(cluster))
610+
for _, cookie := range resp.Cookies() {
611+
if cookie.Name == want {
612+
return cookie.Value, true
613+
}
614+
}
615+
616+
return "", false
617+
}
618+
607619
var oauthSuccessBody = map[string]any{
608620
"access_token": "AT",
609621
"token_type": "Bearer",
@@ -795,6 +807,131 @@ func TestRefreshAndCacheNewToken_TokenError(t *testing.T) {
795807
assert.Len(t, fc.setWithTTLCalls, 0)
796808
}
797809

810+
func TestRefreshAndSetToken_DefaultsToIDToken(t *testing.T) {
811+
const (
812+
oldToken = "OLD"
813+
cluster = "test"
814+
)
815+
816+
fc := &fakeCache{store: map[string]interface{}{"oidc-token-" + oldToken: "REFRESH_OLD"}}
817+
818+
srv := newOIDCProviderServer(t, func(w http.ResponseWriter, r *http.Request) {
819+
require.NoError(t, r.ParseForm())
820+
require.Equal(t, "refresh_token", r.PostForm.Get("grant_type"))
821+
require.Equal(t, "REFRESH_OLD", r.PostForm.Get("refresh_token"))
822+
823+
w.Header().Set("Content-Type", "application/json")
824+
require.NoError(t, json.NewEncoder(w).Encode(oauthSuccessBody))
825+
})
826+
827+
req := httptest.NewRequest(http.MethodGet, "/clusters/"+cluster, nil)
828+
rr := httptest.NewRecorder()
829+
830+
auth.RefreshAndSetToken(auth.RefreshAndSetTokenParams{
831+
Ctx: context.Background(),
832+
OIDCAuthConfig: &kubeconfig.OidcConfig{ClientID: "cid", ClientSecret: "secret", IdpIssuerURL: srv.URL},
833+
Cache: fc,
834+
Token: oldToken,
835+
Cluster: cluster,
836+
Writer: rr,
837+
Request: req,
838+
TelemetryHandler: &telemetry.RequestHandler{},
839+
OIDCIdpIssuerURL: "",
840+
BaseURL: "",
841+
})
842+
843+
resp := rr.Result()
844+
defer resp.Body.Close()
845+
846+
cookieVal, ok := findAuthCookie(resp, cluster)
847+
require.True(t, ok, "expected auth cookie to be set")
848+
assert.Equal(t, "NEW", cookieVal)
849+
}
850+
851+
func TestRefreshAndSetToken_UsesAccessToken(t *testing.T) {
852+
const (
853+
oldToken = "OLD"
854+
cluster = "test"
855+
)
856+
857+
fc := &fakeCache{store: map[string]interface{}{"oidc-token-" + oldToken: "REFRESH_OLD"}}
858+
859+
tokenBody := map[string]any{
860+
"access_token": "ACCESS_NEW",
861+
"token_type": "Bearer",
862+
"expires_in": 3600,
863+
"refresh_token": refreshNew,
864+
"id_token": "IGNORED",
865+
}
866+
867+
srv := newOIDCProviderServer(t, func(w http.ResponseWriter, r *http.Request) {
868+
require.NoError(t, r.ParseForm())
869+
require.Equal(t, "REFRESH_OLD", r.PostForm.Get("refresh_token"))
870+
871+
w.Header().Set("Content-Type", "application/json")
872+
require.NoError(t, json.NewEncoder(w).Encode(tokenBody))
873+
})
874+
875+
req := httptest.NewRequest(http.MethodGet, "/clusters/"+cluster, nil)
876+
rr := httptest.NewRecorder()
877+
878+
auth.RefreshAndSetToken(auth.RefreshAndSetTokenParams{
879+
Ctx: context.Background(),
880+
OIDCAuthConfig: &kubeconfig.OidcConfig{ClientID: "cid", ClientSecret: "secret"},
881+
Cache: fc,
882+
Token: oldToken,
883+
Cluster: cluster,
884+
Writer: rr,
885+
Request: req,
886+
TelemetryHandler: &telemetry.RequestHandler{},
887+
OIDCUseAccessToken: true,
888+
OIDCIdpIssuerURL: srv.URL,
889+
BaseURL: "",
890+
})
891+
892+
resp := rr.Result()
893+
defer resp.Body.Close()
894+
895+
cookieVal, ok := findAuthCookie(resp, cluster)
896+
require.True(t, ok, "expected auth cookie to be set")
897+
assert.Equal(t, "ACCESS_NEW", cookieVal)
898+
}
899+
900+
func TestRefreshAndSetToken_ErrorDoesNotSetCookie(t *testing.T) {
901+
const (
902+
oldToken = "OLD"
903+
cluster = "test"
904+
)
905+
906+
fc := &fakeCache{store: map[string]interface{}{"oidc-token-" + oldToken: "REFRESH_OLD"}}
907+
908+
srv := newOIDCProviderServer(t, func(w http.ResponseWriter, _ *http.Request) {
909+
http.Error(w, "token refresh failed", http.StatusInternalServerError)
910+
})
911+
912+
req := httptest.NewRequest(http.MethodGet, "/clusters/"+cluster, nil)
913+
rr := httptest.NewRecorder()
914+
915+
auth.RefreshAndSetToken(auth.RefreshAndSetTokenParams{
916+
Ctx: context.Background(),
917+
OIDCAuthConfig: &kubeconfig.OidcConfig{ClientID: "cid", ClientSecret: "secret"},
918+
Cache: fc,
919+
Token: oldToken,
920+
Cluster: cluster,
921+
Writer: rr,
922+
Request: req,
923+
TelemetryHandler: &telemetry.RequestHandler{},
924+
OIDCIdpIssuerURL: srv.URL,
925+
BaseURL: "",
926+
})
927+
928+
resp := rr.Result()
929+
defer resp.Body.Close()
930+
931+
_, ok := findAuthCookie(resp, cluster)
932+
assert.False(t, ok, "expected no auth cookie to be set on error")
933+
}
934+
798935
// TestConfigureTLSContext_NoConfig tests when both skipTLSVerify and caCert are not set.
799936
func TestConfigureTLSContext_NoConfig(t *testing.T) {
800937
baseCtx := context.Background()

0 commit comments

Comments
 (0)