feat(db): add oauth_clients table for dynamic client registration
CI / build-and-test (push) Has been cancelled

* Introduced oauth_clients table with fields for client_id, client_name, redirect_uris, and created_at.
* Updated agent_persona_parts, agent_persona_skills, agent_persona_guardrails, agent_persona_traits, and arc_stage_parts tables to use unique constraints instead of primary keys for composite indexes.
This commit is contained in:
Hein
2026-05-07 13:30:30 +02:00
parent fb9606ef2b
commit a993859c62
21 changed files with 1223 additions and 5408 deletions
+6
View File
@@ -26,6 +26,12 @@ func (c *DynamicClient) HasRedirectURI(uri string) bool {
return false
}
// ClientStore is the interface implemented by both DynamicClientStore and PostgresClientStore.
type ClientStore interface {
Register(name string, redirectURIs []string) (DynamicClient, error)
Lookup(clientID string) (DynamicClient, bool)
}
// DynamicClientStore holds dynamically registered OAuth clients in memory.
type DynamicClientStore struct {
mu sync.RWMutex
+18
View File
@@ -17,6 +17,22 @@ type contextKey string
const keyIDContextKey contextKey = "auth.key_id"
// wwwAuthenticate returns the value for a WWW-Authenticate header.
// It advertises Bearer and, when a public URL is known, the OAuth metadata URL per RFC 9728.
func wwwAuthenticate(r *http.Request, publicURL string) string {
base := publicURL
if base == "" {
scheme := "https"
if proto := r.Header.Get("X-Forwarded-Proto"); proto != "" {
scheme = strings.ToLower(proto)
} else if r.TLS == nil {
scheme = "http"
}
base = scheme + "://" + r.Host
}
return `Bearer resource_metadata="` + base + `/.well-known/oauth-authorization-server"`
}
func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthRegistry, tokenStore *TokenStore, tracker *AccessTracker, log *slog.Logger) func(http.Handler) http.Handler {
headerName := cfg.HeaderName
if headerName == "" {
@@ -69,6 +85,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
}
}
log.Warn("bearer token rejected", slog.String("remote_addr", remoteAddr))
w.Header().Set("WWW-Authenticate", wwwAuthenticate(r, "")+`, error="invalid_token"`)
http.Error(w, "invalid token or API key", http.StatusUnauthorized)
return
}
@@ -105,6 +122,7 @@ func Middleware(cfg config.AuthConfig, keyring *Keyring, oauthRegistry *OAuthReg
}
}
w.Header().Set("WWW-Authenticate", wwwAuthenticate(r, ""))
http.Error(w, "authentication required", http.StatusUnauthorized)
})
}
+50
View File
@@ -0,0 +1,50 @@
package auth
import (
"context"
"crypto/rand"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
)
// PostgresClientStore persists dynamically registered OAuth clients (RFC 7591) in PostgreSQL.
type PostgresClientStore struct {
pool *pgxpool.Pool
}
func NewPostgresClientStore(pool *pgxpool.Pool) *PostgresClientStore {
return &PostgresClientStore{pool: pool}
}
func (s *PostgresClientStore) Register(name string, redirectURIs []string) (DynamicClient, error) {
b := make([]byte, 16)
if _, err := rand.Read(b); err != nil {
return DynamicClient{}, err
}
clientID := fmt.Sprintf("%x-%x-%x-%x-%x", b[0:4], b[4:6], b[6:8], b[8:10], b[10:])
var client DynamicClient
row := s.pool.QueryRow(context.Background(), `
insert into oauth_clients (client_id, client_name, redirect_uris)
values ($1, $2, $3)
returning client_id, client_name, redirect_uris, created_at
`, clientID, name, redirectURIs)
if err := row.Scan(&client.ClientID, &client.ClientName, &client.RedirectURIs, &client.CreatedAt); err != nil {
return DynamicClient{}, fmt.Errorf("register oauth client: %w", err)
}
return client, nil
}
func (s *PostgresClientStore) Lookup(clientID string) (DynamicClient, bool) {
var client DynamicClient
row := s.pool.QueryRow(context.Background(), `
select client_id, client_name, redirect_uris, created_at
from oauth_clients
where client_id = $1
`, clientID)
if err := row.Scan(&client.ClientID, &client.ClientName, &client.RedirectURIs, &client.CreatedAt); err != nil {
return DynamicClient{}, false
}
return client, true
}