Skip to content

Commit 1797e0d

Browse files
authored
Merge pull request #3494 from skoeva/auth3
backend: auth: Extract ParseClusterAndToken from headlamp.go
2 parents f0baed3 + d2de4a9 commit 1797e0d

File tree

4 files changed

+122
-37
lines changed

4 files changed

+122
-37
lines changed

backend/cmd/headlamp.go

Lines changed: 1 addition & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ import (
3333
"os"
3434
"path"
3535
"path/filepath"
36-
"regexp"
3736
"runtime"
3837
"strings"
3938
"syscall"
@@ -838,30 +837,6 @@ func createHeadlampHandler(config *HeadlampConfig) http.Handler {
838837
return r
839838
}
840839

841-
func parseClusterAndToken(r *http.Request) (string, string) {
842-
cluster := ""
843-
re := regexp.MustCompile(`^/clusters/([^/]+)/.*`)
844-
urlString := r.URL.RequestURI()
845-
846-
matches := re.FindStringSubmatch(urlString)
847-
if len(matches) > 1 {
848-
cluster = matches[1]
849-
}
850-
851-
// Try Authorization header first (for backward compatibility)
852-
token := r.Header.Get("Authorization")
853-
token = strings.TrimPrefix(token, "Bearer ")
854-
855-
// If no auth header, try cookie
856-
if token == "" && cluster != "" {
857-
if cookieToken, err := auth.GetTokenFromCookie(r, cluster); err == nil {
858-
token = cookieToken
859-
}
860-
}
861-
862-
return cluster, token
863-
}
864-
865840
func getExpiryTime(payload map[string]interface{}) (time.Time, error) {
866841
exp, ok := payload["exp"].(float64)
867842
if !ok {
@@ -1157,7 +1132,7 @@ func (c *HeadlampConfig) OIDCTokenRefreshMiddleware(next http.Handler) http.Hand
11571132
}
11581133

11591134
// parse cluster and token
1160-
cluster, token := parseClusterAndToken(r)
1135+
cluster, token := auth.ParseClusterAndToken(r)
11611136
if c.shouldBypassOIDCRefresh(cluster, token, w, r, span, ctx, start, next) {
11621137
return
11631138
}

backend/cmd/headlamp_test.go

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,17 +1001,6 @@ func TestGetOidcCallbackURL(t *testing.T) {
10011001
}
10021002
}
10031003

1004-
func TestParseClusterAndToken(t *testing.T) {
1005-
ctx := context.Background()
1006-
req, err := http.NewRequestWithContext(ctx, "GET", "/clusters/test-cluster/api", nil)
1007-
require.NoError(t, err)
1008-
req.Header.Set("Authorization", "Bearer test-token")
1009-
1010-
cluster, token := parseClusterAndToken(req)
1011-
assert.Equal(t, "test-cluster", cluster)
1012-
assert.Equal(t, "test-token", token)
1013-
}
1014-
10151004
func TestIsTokenAboutToExpire(t *testing.T) {
10161005
// Token that expires in 4 minutes
10171006
header := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."

backend/pkg/auth/auth.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ package auth
1919
import (
2020
"encoding/base64"
2121
"encoding/json"
22+
"net/http"
23+
"regexp"
24+
"strings"
2225
)
2326

2427
// DecodeBase64JSON decodes a base64 URL-encoded JSON string into a map.
@@ -35,3 +38,37 @@ func DecodeBase64JSON(base64JSON string) (map[string]interface{}, error) {
3538

3639
return payloadMap, nil
3740
}
41+
42+
// clusterPathRegex matches /clusters/<cluster>/...
43+
var clusterPathRegex = regexp.MustCompile(`^/clusters/([^/]+)/.*`)
44+
45+
// bearerTokenRegex matches valid bearer tokens as specified by RFC 6750:
46+
// https://datatracker.ietf.org/doc/html/rfc6750#section-2.1
47+
var bearerTokenRegex = regexp.MustCompile(`^[\x21-\x7E]+$`)
48+
49+
// ParseClusterAndToken extracts the cluster name from the URL path and
50+
// the Bearer token from the Authorization header of the HTTP request.
51+
func ParseClusterAndToken(r *http.Request) (string, string) {
52+
cluster := ""
53+
54+
matches := clusterPathRegex.FindStringSubmatch(r.URL.Path)
55+
if len(matches) > 1 {
56+
cluster = matches[1]
57+
}
58+
59+
token := strings.TrimSpace(r.Header.Get("Authorization"))
60+
if strings.Contains(token, ",") {
61+
return cluster, ""
62+
}
63+
64+
const bearerPrefix = "Bearer "
65+
if strings.HasPrefix(strings.ToLower(token), strings.ToLower(bearerPrefix)) {
66+
token = strings.TrimSpace(token[len(bearerPrefix):])
67+
}
68+
69+
if token != "" && !bearerTokenRegex.MatchString(token) {
70+
return cluster, ""
71+
}
72+
73+
return cluster, token
74+
}

backend/pkg/auth/auth_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ limitations under the License.
1717
package auth_test
1818

1919
import (
20+
"context"
21+
"net/http"
2022
"reflect"
2123
"testing"
2224

@@ -89,3 +91,85 @@ func TestDecodeBase64JSON(t *testing.T) {
8991
})
9092
}
9193
}
94+
95+
var parseClusterAndTokenTests = []struct {
96+
name string
97+
url string
98+
authHeader string
99+
wantCluster string
100+
wantToken string
101+
}{
102+
{
103+
name: "standard case",
104+
url: "/clusters/test-cluster/api",
105+
authHeader: "Bearer test-token",
106+
wantCluster: "test-cluster",
107+
wantToken: "test-token",
108+
},
109+
{
110+
name: "lowercase bearer",
111+
url: "/clusters/abc/api",
112+
authHeader: "bearer token-lowercase",
113+
wantCluster: "abc",
114+
wantToken: "token-lowercase",
115+
},
116+
{
117+
name: "uppercase bearer",
118+
url: "/clusters/xyz/api",
119+
authHeader: "BEARER token-upper",
120+
wantCluster: "xyz",
121+
wantToken: "token-upper",
122+
},
123+
{
124+
name: "extra spaces before bearer",
125+
url: "/clusters/extra/api",
126+
authHeader: " Bearer spaced-token",
127+
wantCluster: "extra",
128+
wantToken: "spaced-token",
129+
},
130+
{
131+
name: "not a clusters path",
132+
url: "/no-clusters-prefix/api",
133+
authHeader: "Bearer test-token",
134+
wantCluster: "",
135+
wantToken: "test-token",
136+
},
137+
{
138+
name: "multiple bearer tokens",
139+
url: "/clusters/test/api",
140+
authHeader: "Bearer xxx, Bearer yyy",
141+
wantCluster: "test",
142+
wantToken: "",
143+
},
144+
{
145+
name: "no cluster in path",
146+
url: "/clusters/",
147+
authHeader: "Bearer some-token",
148+
wantCluster: "",
149+
wantToken: "some-token",
150+
},
151+
}
152+
153+
func TestParseClusterAndToken(t *testing.T) {
154+
for _, tt := range parseClusterAndTokenTests {
155+
t.Run(tt.name, func(t *testing.T) {
156+
req, err := http.NewRequestWithContext(context.Background(), "GET", tt.url, nil)
157+
if err != nil {
158+
t.Fatalf("ParseClusterAndToken() error = %v", err)
159+
}
160+
161+
if tt.authHeader != "" {
162+
req.Header.Set("Authorization", tt.authHeader)
163+
}
164+
165+
cluster, token := auth.ParseClusterAndToken(req)
166+
if cluster != tt.wantCluster {
167+
t.Errorf("ParseClusterAndToken() got cluster %q, want %q", cluster, tt.wantCluster)
168+
}
169+
170+
if token != tt.wantToken {
171+
t.Errorf("ParseClusterAndToken() got token = %q, want %q", token, tt.wantToken)
172+
}
173+
})
174+
}
175+
}

0 commit comments

Comments
 (0)