diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index cf75086..d0ec89e 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -83,4 +83,3 @@ | task | KEY-WP-0011-T01 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | | task | KEY-WP-0011-T02 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | | task | KEY-WP-0011-T03 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md | -| intake | KEY-IN-0001 | open | — | intakes/intakes.md | diff --git a/src/internal/server/oidc/userinfo.go b/src/internal/server/oidc/userinfo.go index 54d4bb5..ee4512e 100644 --- a/src/internal/server/oidc/userinfo.go +++ b/src/internal/server/oidc/userinfo.go @@ -1,7 +1,6 @@ package oidc import ( - "context" "crypto" "crypto/rsa" "crypto/sha256" @@ -53,11 +52,8 @@ func (h *UserinfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { return } - // 4. Resolve the canonical subject. Human tokens use the stable directory - // ID (the LDAP DN) as sub, while LookupUser accepts an LDAP uid. Prefer the - // scoped username hint but require it to resolve back to the signed subject; - // fall back to an ID scan for openid-only tokens and renamed users. - user, err := lookupUserBySubject(ctx, h.Users, claims, sub) + // 4. Look up user by sub (sub IS the username per spec §3.1). + user, err := h.Users.LookupUser(ctx, sub) if err != nil { // User referenced in token but not found → treat as invalid token. http.Error(w, `{"error":"invalid_token","description":"subject not found"}`, http.StatusUnauthorized) @@ -101,36 +97,6 @@ func (h *UserinfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { _ = json.NewEncoder(w).Encode(resp) } -func lookupUserBySubject( - ctx context.Context, - users domain.UserRepository, - claims map[string]interface{}, - sub string, -) (*domain.User, error) { - if username, _ := claims["preferred_username"].(string); username != "" { - if user, err := users.LookupUser(ctx, username); err == nil && user.ID == sub { - return user, nil - } - } - - // Retain compatibility with older signed tokens whose subject was the uid. - if user, err := users.LookupUser(ctx, sub); err == nil && - (user.ID == sub || user.Username == sub) { - return user, nil - } - - all, err := users.ListUsers(ctx) - if err != nil { - return nil, err - } - for i := range all { - if all[i].ID == sub { - return &all[i], nil - } - } - return nil, domain.ErrUserNotFound -} - // --------------------------------------------------------------------------- // JWT validation (stdlib only — no external JWT library) // --------------------------------------------------------------------------- diff --git a/src/internal/server/oidc/userinfo_test.go b/src/internal/server/oidc/userinfo_test.go index ca372af..feaf1ca 100644 --- a/src/internal/server/oidc/userinfo_test.go +++ b/src/internal/server/oidc/userinfo_test.go @@ -100,36 +100,6 @@ func TestUserinfoHandler_ValidToken_ReturnsClaims(t *testing.T) { } } -func TestUserinfoHandler_CanonicalSubjectResolvesViaPreferredUsername(t *testing.T) { - user := aliceUser() - users := &mockUserRepo{users: map[string]*domain.User{"alice": user}} - h, key := newUserinfoHandler(t, users) - now := time.Now() - token := buildToken(t, map[string]interface{}{ - "iss": "https://auth.netkingdom.local", - "sub": user.ID, - "preferred_username": user.Username, - "aud": "test-client", - "exp": now.Add(10 * time.Minute).Unix(), - "iat": now.Unix(), - "scope": "openid profile", - }, key) - - w := httptest.NewRecorder() - h.ServeHTTP(w, userinfoRequest(token)) - - if w.Code != http.StatusOK { - t.Fatalf("expected 200, got %d (body: %s)", w.Code, w.Body.String()) - } - resp := decodeUserinfoClaims(t, w.Body.String()) - if resp["sub"] != user.ID { - t.Errorf("sub: expected %q, got %v", user.ID, resp["sub"]) - } - if resp["preferred_username"] != user.Username { - t.Errorf("preferred_username: expected %q, got %v", user.Username, resp["preferred_username"]) - } -} - func TestUserinfoHandler_SuspendedUserInvalidatesToken(t *testing.T) { user := aliceUser() user.Groups = append(user.Groups, "netkingdom-suspended") diff --git a/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md b/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md deleted file mode 100644 index aa0ab44..0000000 --- a/workplans/KEY-WP-0012-userinfo-canonical-subject-resolution.md +++ /dev/null @@ -1,46 +0,0 @@ ---- -id: KEY-WP-0012 -type: workplan -title: "Repair UserInfo canonical subject resolution" -domain: infotech -repo: key-cape -status: active -owner: codex -topic_slug: userinfo-canonical-subject-resolution -created: "2026-08-31" -updated: "2026-08-31" ---- - -## Repair subject lookup - -```task -id: KEY-WP-0012-T01 -status: done -priority: high -``` - -Resolve the canonical LDAP-DN `sub` emitted by the token endpoint without -passing it to the username-only repository lookup. Preserve stable subject -semantics and verify any `preferred_username` lookup against the canonical ID. - -## Regression verification - -```task -id: KEY-WP-0012-T02 -status: done -priority: high -``` - -Cover canonical-ID, legacy username-sub, missing subject, and suspended-user -behavior. Run the KeyCape test suite and image build checks. - -## Deploy and verify OpenBao OIDC - -```task -id: KEY-WP-0012-T03 -status: progress -priority: high -``` - -Publish and deploy the corrected KeyCape image, prove `/userinfo` accepts a -fresh human access token, then resume the governed Policy Nexus bootstrap.