more platform
This commit is contained in:
1 parent
8a71e32149
commit
2a66754f9e
27 files changed
+2435
-540
No files matched your search
@@ -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
|
||||
}
|
||||
Reference in new issue
Block a user