Skip to content

Commit c719dc1

Browse files
Collin Walkerlets-call-n-walk
authored andcommitted
review fixes
Signed-off-by: Collin Walker <lets-call-n-walk@users.noreply.github.com>
1 parent fcb1244 commit c719dc1

10 files changed

Lines changed: 125 additions & 149 deletions

File tree

go/internal/httpserver/auth/proxy_authn.go

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,15 @@ func (a *ProxyAuthenticator) Authenticate(ctx context.Context, reqHeaders http.H
4444
return nil, ErrUnauthenticated
4545
}
4646

47+
userID := a.getStringClaim(rawClaims, a.claims.UserID, "sub")
48+
if userID == "" {
49+
return nil, ErrUnauthenticated
50+
}
51+
4752
return &SimpleSession{
4853
P: auth.Principal{
4954
User: auth.User{
50-
ID: a.getStringClaim(rawClaims, a.claims.UserID, "sub"),
55+
ID: userID,
5156
Email: a.getStringClaim(rawClaims, a.claims.Email, "email"),
5257
Name: a.getStringClaim(rawClaims, a.claims.Name, "name", "preferred_username"),
5358
},
@@ -60,7 +65,12 @@ func (a *ProxyAuthenticator) Authenticate(ctx context.Context, reqHeaders http.H
6065
}, nil
6166
}
6267

63-
// Fall back to service account auth for internal agent-to-controller calls
68+
// Fall back to service account auth for internal agent-to-controller calls.
69+
// Requires X-Agent-Name to identify the calling agent.
70+
if agentID == "" {
71+
return nil, ErrUnauthenticated
72+
}
73+
6474
// Agents authenticate via user_id query param or X-User-Id header
6575
userID := query.Get("user_id")
6676
if userID == "" {

go/internal/httpserver/auth/proxy_authn_test.go

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,13 @@ func TestProxyAuthenticator_Authenticate(t *testing.T) {
131131
wantGroups: []string{},
132132
wantErr: false,
133133
},
134+
{
135+
name: "returns error when JWT has empty sub claim",
136+
claims: map[string]any{
137+
"email": "user@example.com",
138+
},
139+
wantErr: true,
140+
},
134141
{
135142
name: "handles single group in array",
136143
claims: map[string]any{
@@ -273,37 +280,38 @@ func TestProxyAuthenticator_ServiceAccountFallback(t *testing.T) {
273280
wantErr bool
274281
}{
275282
{
276-
name: "authenticates via user_id query param",
283+
name: "authenticates via user_id query param with agent name",
277284
queryParams: map[string]string{
278285
"user_id": "system:serviceaccount:kagent:kebab-agent",
279286
},
280-
wantUserID: "system:serviceaccount:kagent:kebab-agent",
281-
wantErr: false,
282-
},
283-
{
284-
name: "authenticates via X-User-Id header",
285287
headers: map[string]string{
286-
"X-User-Id": "system:serviceaccount:kagent:test-agent",
288+
"X-Agent-Name": "kagent/kebab-agent",
287289
},
288-
wantUserID: "system:serviceaccount:kagent:test-agent",
289-
wantErr: false,
290+
wantUserID: "system:serviceaccount:kagent:kebab-agent",
291+
wantAgentID: "kagent/kebab-agent",
292+
wantErr: false,
290293
},
291294
{
292-
name: "extracts agent identity from X-Agent-Name header",
293-
queryParams: map[string]string{
294-
"user_id": "system:serviceaccount:kagent:kebab-agent",
295-
},
295+
name: "authenticates via X-User-Id header with agent name",
296296
headers: map[string]string{
297-
"X-Agent-Name": "kagent/kebab-agent",
297+
"X-User-Id": "system:serviceaccount:kagent:test-agent",
298+
"X-Agent-Name": "kagent/test-agent",
298299
},
299-
wantUserID: "system:serviceaccount:kagent:kebab-agent",
300-
wantAgentID: "kagent/kebab-agent",
300+
wantUserID: "system:serviceaccount:kagent:test-agent",
301+
wantAgentID: "kagent/test-agent",
301302
wantErr: false,
302303
},
303304
{
304305
name: "returns error when no auth method available",
305306
wantErr: true,
306307
},
308+
{
309+
name: "returns error when no X-Agent-Name header for fallback",
310+
queryParams: map[string]string{
311+
"user_id": "system:serviceaccount:kagent:kebab-agent",
312+
},
313+
wantErr: true,
314+
},
307315
}
308316

309317
for _, tt := range tests {

go/internal/httpserver/handlers/current_user.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package handlers
22

33
import (
4-
"encoding/json"
54
"net/http"
65

76
"github.com/kagent-dev/kagent/go/pkg/auth"
@@ -35,6 +34,5 @@ func (h *CurrentUserHandler) HandleGetCurrentUser(w http.ResponseWriter, r *http
3534
Groups: principal.Groups,
3635
}
3736

38-
w.Header().Set("Content-Type", "application/json")
39-
json.NewEncoder(w).Encode(response)
37+
RespondWithJSON(w, http.StatusOK, response)
4038
}

go/internal/httpserver/handlers/current_user_test.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,14 @@ func TestHandleGetCurrentUser(t *testing.T) {
8585
if response.Name != tt.wantName {
8686
t.Errorf("Name = %q, want %q", response.Name, tt.wantName)
8787
}
88+
if len(response.Groups) != len(tt.wantGroups) {
89+
t.Errorf("Groups length = %d, want %d", len(response.Groups), len(tt.wantGroups))
90+
}
91+
for i, g := range response.Groups {
92+
if i < len(tt.wantGroups) && g != tt.wantGroups[i] {
93+
t.Errorf("Groups[%d] = %q, want %q", i, g, tt.wantGroups[i])
94+
}
95+
}
8896
}
8997
})
9098
}

go/test/e2e/auth_api_test.go

Lines changed: 64 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package e2e_test
22

33
import (
44
"context"
5+
"encoding/base64"
56
"encoding/json"
67
"io"
78
"net/http"
@@ -13,6 +14,16 @@ import (
1314
"github.com/stretchr/testify/require"
1415
)
1516

17+
// makeTestJWT builds a minimal unsigned JWT (alg:none) with the given claims.
18+
// This is sufficient for proxy mode testing where the oauth2-proxy has already
19+
// validated the token and the backend only parses claims without verification.
20+
func makeTestJWT(claims map[string]any) string {
21+
header := base64.RawURLEncoding.EncodeToString([]byte(`{"alg":"none","typ":"JWT"}`))
22+
payload, _ := json.Marshal(claims)
23+
payloadB64 := base64.RawURLEncoding.EncodeToString(payload)
24+
return header + "." + payloadB64 + "."
25+
}
26+
1627
// CurrentUserResponse mirrors the response from GET /api/me
1728
type CurrentUserResponse struct {
1829
User string `json:"user"`
@@ -31,19 +42,19 @@ func kagentURL() string {
3142
}
3243

3344
// detectAuthMode probes /api/me to determine if the deployment is in proxy or unsecure mode.
45+
// Sends a JWT Bearer token; in proxy mode the backend parses the JWT and returns the sub claim.
46+
// In unsecure mode the backend ignores the Bearer token and returns the default user.
3447
// Returns "proxy" if proxy mode, "unsecure" otherwise.
3548
func detectAuthMode(t *testing.T) string {
3649
t.Helper()
3750

3851
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
3952
defer cancel()
4053

41-
// Create request with X-Forwarded-User but no X-User-Id
42-
// In proxy mode: will return the forwarded user
43-
// In unsecure mode: will return default user (ignores X-Forwarded-User)
54+
token := makeTestJWT(map[string]any{"sub": "probe-user"})
4455
req, err := http.NewRequestWithContext(ctx, "GET", kagentURL()+"/api/me", nil)
4556
require.NoError(t, err)
46-
req.Header.Set("X-Forwarded-User", "probe-user")
57+
req.Header.Set("Authorization", "Bearer "+token)
4758

4859
resp, err := http.DefaultClient.Do(req)
4960
require.NoError(t, err)
@@ -173,13 +184,16 @@ func TestE2EAuthProxyMode(t *testing.T) {
173184
t.Skip("Skipping proxy mode tests - deployment is in unsecure mode")
174185
}
175186

176-
t.Run("full_headers", func(t *testing.T) {
177-
// GET /api/me with all X-Forwarded-* headers
187+
t.Run("full_claims", func(t *testing.T) {
188+
// JWT with all standard claims
189+
token := makeTestJWT(map[string]any{
190+
"sub": "john",
191+
"email": "john@example.com",
192+
"name": "John Doe",
193+
"groups": []string{"admin", "developers"},
194+
})
178195
resp, body := makeAuthRequest(t, map[string]string{
179-
"X-Forwarded-User": "john",
180-
"X-Forwarded-Email": "john@example.com",
181-
"X-Forwarded-Preferred-Username": "John Doe",
182-
"X-Forwarded-Groups": "admin,developers",
196+
"Authorization": "Bearer " + token,
183197
}, nil)
184198
require.Equal(t, http.StatusOK, resp.StatusCode)
185199

@@ -190,10 +204,13 @@ func TestE2EAuthProxyMode(t *testing.T) {
190204
require.ElementsMatch(t, []string{"admin", "developers"}, userResp.Groups)
191205
})
192206

193-
t.Run("minimal_headers", func(t *testing.T) {
194-
// GET /api/me with only required X-Forwarded-User header
207+
t.Run("minimal_claims", func(t *testing.T) {
208+
// JWT with only sub claim
209+
token := makeTestJWT(map[string]any{
210+
"sub": "jane",
211+
})
195212
resp, body := makeAuthRequest(t, map[string]string{
196-
"X-Forwarded-User": "jane",
213+
"Authorization": "Bearer " + token,
197214
}, nil)
198215
require.Equal(t, http.StatusOK, resp.StatusCode)
199216

@@ -204,35 +221,56 @@ func TestE2EAuthProxyMode(t *testing.T) {
204221
require.Empty(t, userResp.Groups)
205222
})
206223

207-
t.Run("missing_required_header_returns_401", func(t *testing.T) {
208-
// GET /api/me without X-Forwarded-User should return 401
224+
t.Run("missing_sub_claim_returns_401", func(t *testing.T) {
225+
// JWT without sub claim should return 401
226+
token := makeTestJWT(map[string]any{
227+
"email": "test@example.com",
228+
})
209229
resp, _ := makeAuthRequest(t, map[string]string{
210-
"X-Forwarded-Email": "test@example.com", // email but no user
230+
"Authorization": "Bearer " + token,
211231
}, nil)
212232
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
213233
})
214234

215-
t.Run("group_parsing_trims_whitespace", func(t *testing.T) {
216-
// Groups with whitespace around commas should be trimmed
235+
t.Run("no_bearer_token_returns_401", func(t *testing.T) {
236+
// No Authorization header and no agent identity should return 401
237+
resp, _ := makeAuthRequest(t, nil, nil)
238+
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
239+
})
240+
241+
t.Run("comma_separated_groups_string", func(t *testing.T) {
242+
// Groups as comma-separated string should be parsed and trimmed
243+
token := makeTestJWT(map[string]any{
244+
"sub": "user",
245+
"groups": "admin, dev , qa ",
246+
})
217247
resp, body := makeAuthRequest(t, map[string]string{
218-
"X-Forwarded-User": "user",
219-
"X-Forwarded-Groups": "admin, dev , qa ",
248+
"Authorization": "Bearer " + token,
220249
}, nil)
221250
require.Equal(t, http.StatusOK, resp.StatusCode)
222251

223252
userResp := parseUserResponse(t, body)
224253
require.ElementsMatch(t, []string{"admin", "dev", "qa"}, userResp.Groups)
225254
})
226255

227-
t.Run("empty_groups_header", func(t *testing.T) {
228-
// Empty groups header should result in empty groups slice
256+
t.Run("agent_fallback_with_user_id", func(t *testing.T) {
257+
// Agent callback: X-Agent-Name + user_id query param (no Bearer token)
229258
resp, body := makeAuthRequest(t, map[string]string{
230-
"X-Forwarded-User": "user",
231-
"X-Forwarded-Groups": "",
232-
}, nil)
259+
"X-Agent-Name": "kagent/test-agent",
260+
}, map[string]string{
261+
"user_id": "owner@example.com",
262+
})
233263
require.Equal(t, http.StatusOK, resp.StatusCode)
234264

235265
userResp := parseUserResponse(t, body)
236-
require.Empty(t, userResp.Groups)
266+
require.Equal(t, "owner@example.com", userResp.User)
267+
})
268+
269+
t.Run("fallback_without_agent_name_returns_401", func(t *testing.T) {
270+
// user_id query param without X-Agent-Name should return 401
271+
resp, _ := makeAuthRequest(t, nil, map[string]string{
272+
"user_id": "owner@example.com",
273+
})
274+
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
237275
})
238276
}

helm/kagent/templates/networkpolicy.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ spec:
4949
matchLabels:
5050
app.kubernetes.io/name: oauth2-proxy
5151
app.kubernetes.io/instance: {{ .Release.Name }}
52+
{{- with .Values.networkPolicy.oauth2ProxySelector }}
53+
{{- toYaml . | nindent 14 }}
54+
{{- end }}
5255
ports:
5356
- protocol: TCP
5457
port: {{ .Values.controller.service.ports.targetPort }}

helm/kagent/values.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,8 +485,6 @@ networkPolicy:
485485
enabled: true
486486
# Additional pod selector labels for oauth2-proxy (if using custom labels)
487487
oauth2ProxySelector: {}
488-
# Additional namespaces to allow traffic from (e.g., for monitoring)
489-
additionalAllowedNamespaces: []
490488

491489
# ==============================================================================
492490
# OBSERVABILITY

ui/public/login-bg.jpg

-970 KB
Binary file not shown.

0 commit comments

Comments
 (0)