@@ -20,6 +20,8 @@ import (
20
20
"bytes"
21
21
"compress/gzip"
22
22
"context"
23
+ "crypto/rand"
24
+ "crypto/sha256"
23
25
"crypto/tls"
24
26
"encoding/base64"
25
27
"encoding/json"
@@ -75,6 +77,7 @@ type HeadlampConfig struct {
75
77
oidcIdpIssuerURL string
76
78
oidcValidatorIdpIssuerURL string
77
79
oidcUseAccessToken bool
80
+ oidcUsePKCE bool
78
81
cache cache.Cache [interface {}]
79
82
multiplexer * Multiplexer
80
83
telemetryConfig cfg.Config
@@ -113,9 +116,29 @@ type spaHandler struct {
113
116
}
114
117
115
118
type OauthConfig struct {
116
- Config * oauth2.Config
117
- Verifier * oidc.IDTokenVerifier
118
- Ctx context.Context
119
+ Config * oauth2.Config
120
+ Verifier * oidc.IDTokenVerifier
121
+ Ctx context.Context
122
+ CodeVerifier string // PKCE code verifier
123
+ }
124
+
125
+ // generateCodeVerifier generates a PKCE code verifier (high-entropy cryptographic random string).
126
+ func generateCodeVerifier () (string , error ) {
127
+ // RFC 7636 recommends 43-128 characters for code verifier. We use 43 characters.
128
+ // Each character is a base64url character, so we need 32 random bytes to get 43 chars
129
+ bytes := make ([]byte , 32 )
130
+ if _ , err := rand .Read (bytes ); err != nil {
131
+ return "" , err
132
+ }
133
+ // Use base64url encoding without padding
134
+ return base64 .RawURLEncoding .EncodeToString (bytes ), nil
135
+ }
136
+
137
+ // generateCodeChallenge generates a PKCE code challenge from the verifier using SHA256.
138
+ func generateCodeChallenge (verifier string ) string {
139
+ hash := sha256 .Sum256 ([]byte (verifier ))
140
+ // Use base64url encoding without padding
141
+ return base64 .RawURLEncoding .EncodeToString (hash [:])
119
142
}
120
143
121
144
func (h spaHandler ) ServeHTTP (w http.ResponseWriter , r * http.Request ) {
@@ -664,12 +687,53 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
664
687
RedirectURL : getOidcCallbackURL (r , config ),
665
688
Scopes : append ([]string {oidc .ScopeOpenID }, oidcAuthConfig .Scopes ... ),
666
689
}
667
- /* we encode the cluster to base64 and set it as state so that when getting redirected
668
- by oidc we can use this state value to get cluster name
669
- */
670
- state := base64 .StdEncoding .EncodeToString ([]byte (cluster ))
671
- oauthRequestMap [state ] = & OauthConfig {Config : oauthConfig , Verifier : verifier , Ctx : ctx }
672
- http .Redirect (w , r , oauthConfig .AuthCodeURL (state ), http .StatusFound )
690
+
691
+ var codeVerifier string
692
+ var authURL string
693
+
694
+ // Generate PKCE parameters if enabled
695
+ if config .oidcUsePKCE {
696
+ var err error
697
+ codeVerifier , err = generateCodeVerifier ()
698
+ if err != nil {
699
+ logger .Log (logger .LevelError , map [string ]string {"cluster" : cluster },
700
+ err , "failed to generate PKCE code verifier" )
701
+ http .Error (w , "Failed to generate PKCE parameters: " + err .Error (), http .StatusInternalServerError )
702
+ return
703
+ }
704
+ codeChallenge := generateCodeChallenge (codeVerifier )
705
+
706
+ /* we encode the cluster to base64 and set it as state so that when getting redirected
707
+ by oidc we can use this state value to get cluster name
708
+ */
709
+ state := base64 .StdEncoding .EncodeToString ([]byte (cluster ))
710
+ oauthRequestMap [state ] = & OauthConfig {
711
+ Config : oauthConfig ,
712
+ Verifier : verifier ,
713
+ Ctx : ctx ,
714
+ CodeVerifier : codeVerifier ,
715
+ }
716
+
717
+ // Create authorization URL with PKCE parameters
718
+ authURL = oauthConfig .AuthCodeURL (state ,
719
+ oauth2 .SetAuthURLParam ("code_challenge" , codeChallenge ),
720
+ oauth2 .SetAuthURLParam ("code_challenge_method" , "S256" ))
721
+ } else {
722
+ /* we encode the cluster to base64 and set it as state so that when getting redirected
723
+ by oidc we can use this state value to get cluster name
724
+ */
725
+ state := base64 .StdEncoding .EncodeToString ([]byte (cluster ))
726
+ oauthRequestMap [state ] = & OauthConfig {
727
+ Config : oauthConfig ,
728
+ Verifier : verifier ,
729
+ Ctx : ctx ,
730
+ }
731
+
732
+ // Create standard authorization URL
733
+ authURL = oauthConfig .AuthCodeURL (state )
734
+ }
735
+
736
+ http .Redirect (w , r , authURL , http .StatusFound )
673
737
}).Queries ("cluster" , "{cluster}" )
674
738
675
739
r .HandleFunc ("/portforward" , func (w http.ResponseWriter , r * http.Request ) {
@@ -711,7 +775,26 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
711
775
712
776
//nolint:nestif
713
777
if oauthConfig , ok := oauthRequestMap [state ]; ok {
714
- oauth2Token , err := oauthConfig .Config .Exchange (oauthConfig .Ctx , r .URL .Query ().Get ("code" ))
778
+ var oauth2Token * oauth2.Token
779
+
780
+ var err error
781
+
782
+ // Exchange authorization code for token, with or without PKCE
783
+ if config .oidcUsePKCE && oauthConfig .CodeVerifier != "" {
784
+ // Use PKCE code verifier for token exchange
785
+ oauth2Token , err = oauthConfig .Config .Exchange (
786
+ oauthConfig .Ctx ,
787
+ r .URL .Query ().Get ("code" ),
788
+ oauth2 .SetAuthURLParam ("code_verifier" , oauthConfig .CodeVerifier ),
789
+ )
790
+ } else {
791
+ // Standard token exchange without PKCE
792
+ oauth2Token , err = oauthConfig .Config .Exchange (
793
+ oauthConfig .Ctx ,
794
+ r .URL .Query ().Get ("code" ),
795
+ )
796
+ }
797
+
715
798
if err != nil {
716
799
logger .Log (logger .LevelError , nil , err , "failed to exchange token" )
717
800
http .Error (w , "Failed to exchange token: " + err .Error (), http .StatusInternalServerError )
@@ -773,6 +856,10 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
773
856
}
774
857
775
858
redirectURL += fmt .Sprintf ("auth?cluster=%1s&token=%2s" , decodedState , rawUserToken )
859
+
860
+ // Clean up the OAuth state to prevent memory leaks
861
+ delete (oauthRequestMap , state )
862
+
776
863
http .Redirect (w , r , redirectURL , http .StatusSeeOther )
777
864
} else {
778
865
http .Error (w , "invalid request" , http .StatusBadRequest )
0 commit comments