Fix OpenBao OIDC token exchange compatibility
Some checks failed
Build and Publish Container Image / build-and-push (push) Has been cancelled

This commit is contained in:
2026-06-01 21:20:54 +02:00
parent 06d20c3379
commit d6d41dd84f
6 changed files with 43 additions and 11 deletions

View File

@@ -125,11 +125,16 @@ func (a *LDAPAdapter) LookupUser(ctx context.Context, username string) (*domain.
entry := result.Entries[0] entry := result.Entries[0]
user := mapEntryToUser(entry) 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}} snap := validator.Snapshot{Users: []domain.User{user}}
report := validator.Validate(snap, validator.ModeProvisioning) report := validator.Validate(snap, validator.ModeProvisioning)
if !report.Passed { 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 return &user, nil

View File

@@ -154,16 +154,20 @@ func TestLookupUser_NotFound(t *testing.T) {
} }
} }
func TestLookupUser_ValidationFailure(t *testing.T) { func TestLookupUser_ValidationWarningDoesNotBlockRuntimeLogin(t *testing.T) {
// Return an entry with an empty DisplayName and empty sn — will fail validator. // Return an entry with an empty DisplayName and empty sn. Runtime login
dn := "uid=broken,ou=users,dc=netkingdom,dc=local" // should still resolve the user; provisioning validators report the warning.
dn := "uid=platform-root,ou=people,dc=netkingdom,dc=local"
conn := &mockConn{ conn := &mockConn{
searchFn: func(req *ldap.SearchRequest) (*ldap.SearchResult, error) { 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{ attrs := []*ldap.EntryAttribute{
{Name: "uid", Values: []string{"broken"}}, {Name: "uid", Values: []string{"platform-root"}},
{Name: "cn", Values: []string{""}}, {Name: "cn", Values: []string{""}},
{Name: "sn", Values: []string{""}}, {Name: "sn", Values: []string{""}},
{Name: "mail", Values: []string{"broken@example.com"}}, {Name: "mail", Values: []string{"bernd.worsch@gmail.com"}},
} }
return &ldap.SearchResult{ return &ldap.SearchResult{
Entries: []*ldap.Entry{{DN: dn, Attributes: attrs}}, Entries: []*ldap.Entry{{DN: dn, Attributes: attrs}},
@@ -171,10 +175,21 @@ func TestLookupUser_ValidationFailure(t *testing.T) {
}, },
} }
adapter := makeAdapter(testConfig(), conn) cfg := testConfig()
_, err := adapter.LookupUser(context.Background(), "broken") cfg.UserOU = "ou=people"
if err == nil { adapter := makeAdapter(cfg, conn)
t.Fatal("expected validation error, got nil") 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")
} }
} }

View File

@@ -23,6 +23,7 @@ type PendingState struct {
PKCEChallenge string PKCEChallenge string
PKCEChallengeMethod string PKCEChallengeMethod string
State string State string
Nonce string
Scopes []string Scopes []string
ExpiresAt time.Time ExpiresAt time.Time
AuthenticatedUser string AuthenticatedUser string
@@ -103,6 +104,7 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request
responseType := q.Get("response_type") responseType := q.Get("response_type")
scope := q.Get("scope") scope := q.Get("scope")
state := q.Get("state") state := q.Get("state")
nonce := q.Get("nonce")
codeChallenge := q.Get("code_challenge") codeChallenge := q.Get("code_challenge")
codeChallengeMethod := q.Get("code_challenge_method") codeChallengeMethod := q.Get("code_challenge_method")
@@ -191,6 +193,7 @@ func (h *AuthorizeHandler) serveAuthorize(w http.ResponseWriter, r *http.Request
PKCEChallenge: codeChallenge, PKCEChallenge: codeChallenge,
PKCEChallengeMethod: codeChallengeMethod, PKCEChallengeMethod: codeChallengeMethod,
State: state, State: state,
Nonce: nonce,
Scopes: strings.Fields(scope), Scopes: strings.Fields(scope),
ExpiresAt: time.Now().Add(10 * time.Minute), ExpiresAt: time.Now().Add(10 * time.Minute),
}) })
@@ -358,6 +361,7 @@ func (h *AuthorizeHandler) completeAuthorization(w http.ResponseWriter, r *http.
PKCEChallenge: ps.PKCEChallenge, PKCEChallenge: ps.PKCEChallenge,
PKCEChallengeMethod: ps.PKCEChallengeMethod, PKCEChallengeMethod: ps.PKCEChallengeMethod,
State: ps.State, State: ps.State,
Nonce: ps.Nonce,
Username: username, Username: username,
Scopes: ps.Scopes, Scopes: ps.Scopes,
ExpiresAt: time.Now().Add(10 * time.Minute), ExpiresAt: time.Now().Add(10 * time.Minute),

View File

@@ -15,6 +15,7 @@ type PKCESession struct {
PKCEChallenge string // S256 challenge PKCEChallenge string // S256 challenge
PKCEChallengeMethod string // always "S256" PKCEChallengeMethod string // always "S256"
State string State string
Nonce string
Username string // set after auth Username string // set after auth
Scopes []string Scopes []string
ExpiresAt time.Time ExpiresAt time.Time

View File

@@ -111,6 +111,9 @@ func (h *TokenHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
"exp": exp.Unix(), "exp": exp.Unix(),
"iat": now.Unix(), "iat": now.Unix(),
} }
if sess.Nonce != "" {
claims["nonce"] = sess.Nonce
}
scopeSet := make(map[string]bool) scopeSet := make(map[string]bool)
for _, s := range sess.Scopes { for _, s := range sess.Scopes {

View File

@@ -107,6 +107,7 @@ func seededSession(sessions *oidc.SessionStore, verifier string) (code string) {
PKCEChallenge: challenge, PKCEChallenge: challenge,
PKCEChallengeMethod: "S256", PKCEChallengeMethod: "S256",
State: "state1", State: "state1",
Nonce: "nonce1",
Username: "alice", Username: "alice",
Scopes: []string{"openid", "profile", "email", "groups"}, Scopes: []string{"openid", "profile", "email", "groups"},
ExpiresAt: time.Now().Add(10 * time.Minute), ExpiresAt: time.Now().Add(10 * time.Minute),
@@ -323,6 +324,9 @@ func TestTokenHandler_JWTClaims_CorrectSubAndIssuer(t *testing.T) {
if claims["aud"] != "test-client" { if claims["aud"] != "test-client" {
t.Errorf("aud: expected test-client, got %v", claims["aud"]) 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) { func TestTokenHandler_ScopeFiltering_ProfileScope(t *testing.T) {