Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 97 additions & 10 deletions backend/cmd/headlamp.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"bytes"
"compress/gzip"
"context"
"crypto/rand"
"crypto/sha256"
"crypto/tls"
"encoding/base64"
"encoding/json"
Expand Down Expand Up @@ -75,6 +77,7 @@ type HeadlampConfig struct {
oidcIdpIssuerURL string
oidcValidatorIdpIssuerURL string
oidcUseAccessToken bool
oidcUsePKCE bool
cache cache.Cache[interface{}]
multiplexer *Multiplexer
telemetryConfig cfg.Config
Expand Down Expand Up @@ -113,9 +116,29 @@ type spaHandler struct {
}

type OauthConfig struct {
Config *oauth2.Config
Verifier *oidc.IDTokenVerifier
Ctx context.Context
Config *oauth2.Config
Verifier *oidc.IDTokenVerifier
Ctx context.Context
CodeVerifier string // PKCE code verifier
}

// generateCodeVerifier generates a PKCE code verifier (high-entropy cryptographic random string).
func generateCodeVerifier() (string, error) {
// RFC 7636 recommends 43-128 characters for code verifier. We use 43 characters.
// Each character is a base64url character, so we need 32 random bytes to get 43 chars
bytes := make([]byte, 32)
if _, err := rand.Read(bytes); err != nil {
return "", err
}
// Use base64url encoding without padding
return base64.RawURLEncoding.EncodeToString(bytes), nil
}

// generateCodeChallenge generates a PKCE code challenge from the verifier using SHA256.
func generateCodeChallenge(verifier string) string {
hash := sha256.Sum256([]byte(verifier))
// Use base64url encoding without padding
return base64.RawURLEncoding.EncodeToString(hash[:])
}

func (h spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -664,12 +687,53 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
RedirectURL: getOidcCallbackURL(r, config),
Scopes: append([]string{oidc.ScopeOpenID}, oidcAuthConfig.Scopes...),
}
/* we encode the cluster to base64 and set it as state so that when getting redirected
by oidc we can use this state value to get cluster name
*/
state := base64.StdEncoding.EncodeToString([]byte(cluster))
oauthRequestMap[state] = &OauthConfig{Config: oauthConfig, Verifier: verifier, Ctx: ctx}
http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound)

var codeVerifier string
var authURL string

// Generate PKCE parameters if enabled
if config.oidcUsePKCE {
var err error
codeVerifier, err = generateCodeVerifier()
if err != nil {
logger.Log(logger.LevelError, map[string]string{"cluster": cluster},
err, "failed to generate PKCE code verifier")
http.Error(w, "Failed to generate PKCE parameters: "+err.Error(), http.StatusInternalServerError)
return
}
codeChallenge := generateCodeChallenge(codeVerifier)

/* we encode the cluster to base64 and set it as state so that when getting redirected
by oidc we can use this state value to get cluster name
*/
state := base64.StdEncoding.EncodeToString([]byte(cluster))
oauthRequestMap[state] = &OauthConfig{
Config: oauthConfig,
Verifier: verifier,
Ctx: ctx,
CodeVerifier: codeVerifier,
}

// Create authorization URL with PKCE parameters
authURL = oauthConfig.AuthCodeURL(state,
oauth2.SetAuthURLParam("code_challenge", codeChallenge),
oauth2.SetAuthURLParam("code_challenge_method", "S256"))
} else {
/* we encode the cluster to base64 and set it as state so that when getting redirected
by oidc we can use this state value to get cluster name
*/
state := base64.StdEncoding.EncodeToString([]byte(cluster))
oauthRequestMap[state] = &OauthConfig{
Config: oauthConfig,
Verifier: verifier,
Ctx: ctx,
}

// Create standard authorization URL
authURL = oauthConfig.AuthCodeURL(state)
}

http.Redirect(w, r, authURL, http.StatusFound)
}).Queries("cluster", "{cluster}")

r.HandleFunc("/portforward", func(w http.ResponseWriter, r *http.Request) {
Expand Down Expand Up @@ -711,7 +775,26 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {

//nolint:nestif
if oauthConfig, ok := oauthRequestMap[state]; ok {
oauth2Token, err := oauthConfig.Config.Exchange(oauthConfig.Ctx, r.URL.Query().Get("code"))
var oauth2Token *oauth2.Token

var err error

// Exchange authorization code for token, with or without PKCE
if config.oidcUsePKCE && oauthConfig.CodeVerifier != "" {
// Use PKCE code verifier for token exchange
oauth2Token, err = oauthConfig.Config.Exchange(
oauthConfig.Ctx,
r.URL.Query().Get("code"),
oauth2.SetAuthURLParam("code_verifier", oauthConfig.CodeVerifier),
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oauth2.SetAuthURLParam function is being used incorrectly for token exchange. For PKCE token exchange, you should use oauth2.SetAuthURLParam with the Exchange method, but the parameter should be passed as an option to Exchange, not as an auth URL parameter. Use oauth2.SetAuthURLParam("code_verifier", oauthConfig.CodeVerifier) as an option to the Exchange call.

Copilot uses AI. Check for mistakes.

)
} else {
// Standard token exchange without PKCE
oauth2Token, err = oauthConfig.Config.Exchange(
oauthConfig.Ctx,
r.URL.Query().Get("code"),
)
}

if err != nil {
logger.Log(logger.LevelError, nil, err, "failed to exchange token")
http.Error(w, "Failed to exchange token: "+err.Error(), http.StatusInternalServerError)
Expand Down Expand Up @@ -773,6 +856,10 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
}

redirectURL += fmt.Sprintf("auth?cluster=%1s&token=%2s", decodedState, rawUserToken)

// Clean up the OAuth state to prevent memory leaks
delete(oauthRequestMap, state)

http.Redirect(w, r, redirectURL, http.StatusSeeOther)
} else {
http.Error(w, "invalid request", http.StatusBadRequest)
Expand Down
1 change: 1 addition & 0 deletions backend/cmd/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ func main() {
oidcValidatorIdpIssuerURL: conf.OidcValidatorIdpIssuerURL,
oidcScopes: strings.Split(conf.OidcScopes, ","),
oidcUseAccessToken: conf.OidcUseAccessToken,
oidcUsePKCE: conf.OidcUsePKCE,
cache: cache,
multiplexer: multiplexer,
telemetryConfig: config.Config{
Expand Down
2 changes: 2 additions & 0 deletions backend/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type Config struct {
OidcValidatorIdpIssuerURL string `koanf:"oidc-validator-idp-issuer-url"`
OidcScopes string `koanf:"oidc-scopes"`
OidcUseAccessToken bool `koanf:"oidc-use-access-token"`
OidcUsePKCE bool `koanf:"oidc-use-pkce"`
// telemetry configs
ServiceName string `koanf:"service-name"`
ServiceVersion *string `koanf:"service-version"`
Expand Down Expand Up @@ -260,6 +261,7 @@ func flagset() *flag.FlagSet {
f.String("oidc-scopes", "profile,email",
"A comma separated list of scopes needed from the OIDC provider")
f.Bool("oidc-use-access-token", false, "Setup oidc to pass through the access_token instead of the default id_token")
f.Bool("oidc-use-pkce", false, "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow")
// Telemetry flags.
f.String("service-name", "headlamp", "Service name for telemetry")
f.String("service-version", "0.30.0", "Service version for telemetry")
Expand Down
1 change: 1 addition & 0 deletions charts/headlamp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ $ helm install my-headlamp headlamp/headlamp \
| config.oidc.clientSecret | string | `""` | OIDC client secret |
| config.oidc.issuerURL | string | `""` | OIDC issuer URL |
| config.oidc.scopes | string | `""` | OIDC scopes to be used |
| config.oidc.usePKCE | bool | `false` | Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow |
| config.oidc.secret.create | bool | `true` | Create OIDC secret using provided values |
| config.oidc.secret.name | string | `"oidc"` | Name of the OIDC secret |
| config.oidc.externalSecret.enabled | bool | `false` | Enable using external secret for OIDC |
Expand Down
15 changes: 15 additions & 0 deletions charts/headlamp/templates/deployment.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,13 @@ spec:
name: {{ $oidc.secret.name }}
key: useAccessToken
{{- end }}
{{- if $oidc.usePKCE }}
- name: OIDC_USE_PKCE
valueFrom:
secretKeyRef:
name: {{ $oidc.secret.name }}
key: usePKCE
{{- end }}
{{- else }}
{{- if $oidc.clientID }}
- name: OIDC_CLIENT_ID
Expand Down Expand Up @@ -167,6 +174,10 @@ spec:
- name: OIDC_USE_ACCESS_TOKEN
value: {{ $oidc.useAccessToken }}
{{- end }}
{{- if $oidc.usePKCE }}
- name: OIDC_USE_PKCE
value: {{ $oidc.usePKCE }}
{{- end }}
{{- end }}
{{- if .Values.env }}
{{- toYaml .Values.env | nindent 12 }}
Expand Down Expand Up @@ -213,6 +224,10 @@ spec:
# Check if useAccessToken is non false either from env or oidc.config
- "-oidc-use-access-token=$(OIDC_USE_ACCESS_TOKEN)"
{{- end }}
{{- if or (ne ($oidc.usePKCE | toString) "false") (ne $usePKCE "") }}
Copy link

Copilot AI Jul 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable $usePKCE is referenced but not defined in this context. This appears to be a copy-paste error from the useAccessToken logic above. It should likely be ($oidc.usePKCE | toString) or the environment variable check should be removed.

Suggested change
{{- if or (ne ($oidc.usePKCE | toString) "false") (ne $usePKCE "") }}
{{- if or (ne ($oidc.usePKCE | toString) "false") (ne $usePKCE "false") }}

Copilot uses AI. Check for mistakes.

# Check if usePKCE is non false either from env or oidc.config
- "-oidc-use-pkce=$(OIDC_USE_PKCE)"
{{- end }}
{{- else }}
- "-oidc-client-id=$(OIDC_CLIENT_ID)"
- "-oidc-client-secret=$(OIDC_CLIENT_SECRET)"
Expand Down
3 changes: 3 additions & 0 deletions charts/headlamp/templates/secret.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,8 @@ data:
{{- with .useAccessToken }}
useAccessToken: {{ . | toString | b64enc | quote }}
{{- end }}
{{- with .usePKCE }}
usePKCE: {{ . | toString | b64enc | quote }}
{{- end }}
{{- end }}
{{- end }}
4 changes: 4 additions & 0 deletions charts/headlamp/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,10 @@
"type": "string",
"description": "Scopes of the OIDC provider"
},
"usePKCE": {
"type": "boolean",
"description": "Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow"
},
"externalSecret": {
"type": "object",
"description": "External secret to use for OIDC configuration",
Expand Down
2 changes: 2 additions & 0 deletions charts/headlamp/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ config:
validatorIssuerURL: ""
# -- Use 'access_token' instead of 'id_token' when authenticating using OIDC
useAccessToken: false
# -- Use PKCE (Proof Key for Code Exchange) for enhanced security in OIDC flow
usePKCE: false

# Option 3:
# @param config.oidc - External OIDC secret configuration
Expand Down
Loading