Compare commits
No commits in common. "153258b9d39273d53ca7ab5d1bdc09961ea9026e" and "608c5d20cf862cde0df12ee824aabbfa4d429ec7" have entirely different histories.
153258b9d3
...
608c5d20cf
4 changed files with 2 additions and 113 deletions
|
|
@ -83,4 +83,3 @@
|
||||||
| task | KEY-WP-0011-T01 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
| 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-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 |
|
| task | KEY-WP-0011-T03 | done | — | workplans/KEY-WP-0011-live-secret-exposure-recovery.md |
|
||||||
| intake | KEY-IN-0001 | open | — | intakes/intakes.md |
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package oidc
|
package oidc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"crypto"
|
"crypto"
|
||||||
"crypto/rsa"
|
"crypto/rsa"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
|
|
@ -53,11 +52,8 @@ func (h *UserinfoHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Resolve the canonical subject. Human tokens use the stable directory
|
// 4. Look up user by sub (sub IS the username per spec §3.1).
|
||||||
// ID (the LDAP DN) as sub, while LookupUser accepts an LDAP uid. Prefer the
|
user, err := h.Users.LookupUser(ctx, sub)
|
||||||
// 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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// User referenced in token but not found → treat as invalid token.
|
// User referenced in token but not found → treat as invalid token.
|
||||||
http.Error(w, `{"error":"invalid_token","description":"subject not found"}`, http.StatusUnauthorized)
|
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)
|
_ = 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)
|
// JWT validation (stdlib only — no external JWT library)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
func TestUserinfoHandler_SuspendedUserInvalidatesToken(t *testing.T) {
|
||||||
user := aliceUser()
|
user := aliceUser()
|
||||||
user.Groups = append(user.Groups, "netkingdom-suspended")
|
user.Groups = append(user.Groups, "netkingdom-suspended")
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue