more platform

This commit is contained in:
mats committed 2026-06-29 12:35:32 +02:00
1 parent 8a71e32149
commit 2a66754f9e
27 files changed
+2435 -540

No files matched your search

+87
View File
@@ -0,0 +1,87 @@
// Package auth is the stable cross-service authentication seam for the
// platform. It is provider-agnostic: one Authenticator interface with
// interchangeable impls — OIDC (validates a bearer JWT against any OIDC
// issuer; Zitadel is the default reference), API key (validates X-API-Key
// against the KeyStore), and a Chain combinator that runs them in order.
//
// Packages under platform/ import only the standard library and other
// platform/ packages — never an application module. Application modules
// depend downward onto platform. This is what keeps the auth seam stable
// and reusable.
package auth
import (
"errors"
"net/http"
"slices"
)
// Kind values for Principal.Kind. Comparing against these strings is the
// authorization layer's primitive: handlers ask "is this OIDC or an API
// key?" rather than re-implementing the chain logic.
const (
KindOIDC = "oidc"
KindAPIKey = "apikey"
KindAnonymous = "anonymous"
)
// ErrNoCredentials is returned when a request carries no credentials for the
// authenticator (no bearer token / no API key). Chain treats it like any
// other failure and falls through to the next authenticator.
var ErrNoCredentials = errors.New("auth: no credentials")
// Principal is the authenticated caller. Authorization (scope checks) is a
// thin layer on top: handlers ask "does this principal have scope X", not
// "how did it authenticate".
type Principal struct {
Subject string `json:"subject"` // OIDC sub or API key id
Scopes []string `json:"scopes,omitempty"` // e.g. ["write","admin"]
Kind string `json:"kind"` // KindOIDC | KindAPIKey | KindAnonymous
}
// HasScope reports whether the principal carries the given scope.
func (p *Principal) HasScope(scope string) bool {
return slices.Contains(p.Scopes, scope)
}
// Authenticator inspects request credentials and returns the caller, or an
// error if authentication fails.
type Authenticator interface {
Authenticate(r *http.Request) (*Principal, error)
}
// Chain tries each authenticator in order, returning the first successful
// Principal. It is how bearer-then-apikey composition is expressed.
type Chain []Authenticator
var _ Authenticator = Chain(nil)
// Authenticate returns the first non-error result. On failure it returns
// the first *real* validation error — an authenticator reporting
// ErrNoCredentials merely didn't apply (e.g. no X-API-Key header alongside
// a bad bearer token) and must not mask why the presented credential was
// rejected.
func (c Chain) Authenticate(r *http.Request) (*Principal, error) {
var firstErr error
for _, a := range c {
p, err := a.Authenticate(r)
if err == nil {
return p, nil
}
if firstErr == nil || (errors.Is(firstErr, ErrNoCredentials) && !errors.Is(err, ErrNoCredentials)) {
firstErr = err
}
}
return nil, firstErr
}
// Anonymous authenticates every request as an unauthenticated principal
// with no scopes. It is the placeholder for tests and local-dev runs; it
// must never be wired in production.
type Anonymous struct{}
var _ Authenticator = Anonymous{}
func (Anonymous) Authenticate(*http.Request) (*Principal, error) {
return &Principal{Subject: "anonymous", Kind: KindAnonymous}, nil
}