From d6d41dd84f4ef900964c8e9ca5e0ca6eb793bdba Mon Sep 17 00:00:00 2001 From: tegwick Date: Mon, 1 Jun 2026 21:20:54 +0200 Subject: [PATCH] Fix OpenBao OIDC token exchange compatibility --- src/internal/adapters/lldap/adapter.go | 9 ++++-- src/internal/adapters/lldap/adapter_test.go | 33 +++++++++++++++------ src/internal/server/oidc/authorize.go | 4 +++ src/internal/server/oidc/session.go | 1 + src/internal/server/oidc/token.go | 3 ++ src/internal/server/oidc/token_test.go | 4 +++ 6 files changed, 43 insertions(+), 11 deletions(-) diff --git a/src/internal/adapters/lldap/adapter.go b/src/internal/adapters/lldap/adapter.go index e929564..12c3c93 100644 --- a/src/internal/adapters/lldap/adapter.go +++ b/src/internal/adapters/lldap/adapter.go @@ -125,11 +125,16 @@ func (a *LDAPAdapter) LookupUser(ctx context.Context, username string) (*domain. entry := result.Entries[0] user := mapEntryToUser(entry) - // Run the canonical LDAP schema validator. + // Runtime login should not fail because a live directory entry is missing + // provisioning metadata such as cn/sn. Keep the warning visible for + // diagnostics, but return the resolved user so token issuance can proceed. snap := validator.Snapshot{Users: []domain.User{user}} report := validator.Validate(snap, validator.ModeProvisioning) if !report.Passed { - return nil, fmt.Errorf("lldap: validation failed for user %q: %s", username, validationSummary(report)) + if user.LDAPAttributes == nil { + user.LDAPAttributes = make(map[string]string) + } + user.LDAPAttributes["_validation_warning"] = validationSummary(report) } return &user, nil diff --git a/src/internal/adapters/lldap/adapter_test.go b/src/internal/adapters/lldap/adapter_test.go index b556af4..72c5bb3 100644 --- a/src/internal/adapters/lldap/adapter_test.go +++ b/src/internal/adapters/lldap/adapter_test.go @@ -154,16 +154,20 @@ func TestLookupUser_NotFound(t *testing.T) { } } -func TestLookupUser_ValidationFailure(t *testing.T) { - // Return an entry with an empty DisplayName and empty sn — will fail validator. - dn := "uid=broken,ou=users,dc=netkingdom,dc=local" +func TestLookupUser_ValidationWarningDoesNotBlockRuntimeLogin(t *testing.T) { + // Return an entry with an empty DisplayName and empty sn. Runtime login + // should still resolve the user; provisioning validators report the warning. + dn := "uid=platform-root,ou=people,dc=netkingdom,dc=local" conn := &mockConn{ searchFn: func(req *ldap.SearchRequest) (*ldap.SearchResult, error) { + if req.BaseDN != "ou=people,dc=netkingdom,dc=local" { + t.Fatalf("BaseDN: want ou=people,dc=netkingdom,dc=local, got %q", req.BaseDN) + } attrs := []*ldap.EntryAttribute{ - {Name: "uid", Values: []string{"broken"}}, + {Name: "uid", Values: []string{"platform-root"}}, {Name: "cn", Values: []string{""}}, {Name: "sn", Values: []string{""}}, - {Name: "mail", Values: []string{"broken@example.com"}}, + {Name: "mail", Values: []string{"bernd.worsch@gmail.com"}}, } return &ldap.SearchResult{ Entries: []*ldap.Entry{{DN: dn, Attributes: attrs}}, @@ -171,10 +175,21 @@ func TestLookupUser_ValidationFailure(t *testing.T) { }, } - adapter := makeAdapter(testConfig(), conn) - _, err := adapter.LookupUser(context.Background(), "broken") - if err == nil { - t.Fatal("expected validation error, got nil") + cfg := testConfig() + cfg.UserOU = "ou=people" + adapter := makeAdapter(cfg, conn) + user, err := adapter.LookupUser(context.Background(), "platform-root") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if user.ID != dn { + t.Errorf("ID: want %q, got %q", dn, user.ID) + } + if user.Username != "platform-root" { + t.Errorf("Username: want platform-root, got %q", user.Username) + } + if user.LDAPAttributes["_validation_warning"] == "" { + t.Error("expected validation warning for missing displayName") } } diff --git a/src/internal/server/oidc/authorize.go b/src/internal/server/oidc/authorize.go index b3160b7..94ef3f2 100644 --- a/src/internal/server/oidc/authorize.go +++ b/src/internal/server/oidc/authorize.go @@ -23,6 +23,7 @@ type PendingState struct { PKCEChallenge string PKCEChallengeMethod string State string + Nonce string Scopes []string ExpiresAt time.Time AuthenticatedUser string @@ -103,6 +104,7 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request responseType := q.Get("response_type") scope := q.Get("scope") state := q.Get("state") + nonce := q.Get("nonce") codeChallenge := q.Get("code_challenge") codeChallengeMethod := q.Get("code_challenge_method") @@ -191,6 +193,7 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request PKCEChallenge: codeChallenge, PKCEChallengeMethod: codeChallengeMethod, State: state, + Nonce: nonce, Scopes: strings.Fields(scope), ExpiresAt: time.Now().Add(10 * time.Minute), }) @@ -358,6 +361,7 @@ func (h *AuthorizeHandler) completeAuthorization(w http.ResponseWriter, r *http. PKCEChallenge: ps.PKCEChallenge, PKCEChallengeMethod: ps.PKCEChallengeMethod, State: ps.State, + Nonce: ps.Nonce, Username: username, Scopes: ps.Scopes, ExpiresAt: time.Now().Add(10 * time.Minute), diff --git a/src/internal/server/oidc/session.go b/src/internal/server/oidc/session.go index 75ea6f4..72405b0 100644 --- a/src/internal/server/oidc/session.go +++ b/src/internal/server/oidc/session.go @@ -15,6 +15,7 @@ type PKCESession struct { PKCEChallenge string // S256 challenge PKCEChallengeMethod string // always "S256" State string + Nonce string Username string // set after auth Scopes []string ExpiresAt time.Time diff --git a/src/internal/server/oidc/token.go b/src/internal/server/oidc/token.go index ee0848a..35f1d4b 100644 --- a/src/internal/server/oidc/token.go +++ b/src/internal/server/oidc/token.go @@ -111,6 +111,9 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { "exp": exp.Unix(), "iat": now.Unix(), } + if sess.Nonce != "" { + claims["nonce"] = sess.Nonce + } scopeSet := make(map[string]bool) for _, s := range sess.Scopes { diff --git a/src/internal/server/oidc/token_test.go b/src/internal/server/oidc/token_test.go index 8192c8e..555a432 100644 --- a/src/internal/server/oidc/token_test.go +++ b/src/internal/server/oidc/token_test.go @@ -107,6 +107,7 @@ func seededSession(sessions *oidc.SessionStore, verifier string) (code string) { PKCEChallenge: challenge, PKCEChallengeMethod: "S256", State: "state1", + Nonce: "nonce1", Username: "alice", Scopes: []string{"openid", "profile", "email", "groups"}, ExpiresAt: time.Now().Add(10 * time.Minute), @@ -323,6 +324,9 @@ func TestTokenHandler_JWTClaims_CorrectSubAndIssuer(t *testing.T) { if claims["aud"] != "test-client" { t.Errorf("aud: expected test-client, got %v", claims["aud"]) } + if claims["nonce"] != "nonce1" { + t.Errorf("nonce: expected nonce1, got %v", claims["nonce"]) + } } func TestTokenHandler_ScopeFiltering_ProfileScope(t *testing.T) {