flex-auth/internal/callerauth/auth.go
tegwick ca070df32d
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
Build and Publish Container Image / build-and-push (push) Successful in 1m0s
Record authenticated caller in the decision envelope.
FLEX-WP-0023-T04: provenance.caller is additive (mode required;
principal/audience/not_after when a token was reviewed). TokenReview
keeps the JWT exp. request_digest is unchanged because the caller is
not binding material.

Assistant: grok
Assistant-Session: 01a09dc1-b21e-77e1-919e-fcad2f82b267
2026-09-14 04:44:07 +02:00

178 lines
5.2 KiB
Go

// Package callerauth authenticates protected systems before flex-auth evaluates
// the authorization request they submit.
package callerauth
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
type Mode string
const (
ModeDisabled Mode = "disabled"
ModeWarn Mode = "warn"
ModeEnforce Mode = "enforce"
)
var (
ErrUnauthenticated = errors.New("caller is not authenticated")
ErrForbidden = errors.New("caller is not allowed to represent the requested system")
ErrUnavailable = errors.New("caller identity service is unavailable")
)
type Identity struct {
Username string
Audiences []string
NotAfter time.Time
}
// Record is the caller provenance stamped onto a decision envelope.
// Mode is always set. Principal, audience and expiry are present only when a
// token was actually reviewed.
type Record struct {
Mode Mode
Principal string
Audience string
NotAfter time.Time
}
type recordContextKey struct{}
func WithRecord(ctx context.Context, record Record) context.Context {
return context.WithValue(ctx, recordContextKey{}, record)
}
func RecordFromContext(ctx context.Context) (Record, bool) {
record, ok := ctx.Value(recordContextKey{}).(Record)
return record, ok
}
type TokenReviewer interface {
Review(context.Context, string) (Identity, error)
}
type WarningFunc func(string, ...any)
type Authenticator struct {
mode Mode
reviewer TokenReviewer
audience string
bindings map[string]string
warnf WarningFunc
}
func New(mode Mode, reviewer TokenReviewer, audience string, bindings map[string]string, warnf WarningFunc) (*Authenticator, error) {
switch mode {
case ModeDisabled:
return &Authenticator{mode: mode}, nil
case ModeWarn, ModeEnforce:
default:
return nil, fmt.Errorf("unsupported caller-auth mode %q", mode)
}
if reviewer == nil {
return nil, fmt.Errorf("token reviewer is required in %s mode", mode)
}
if strings.TrimSpace(audience) == "" {
return nil, fmt.Errorf("caller audience is required in %s mode", mode)
}
if len(bindings) == 0 {
return nil, fmt.Errorf("at least one caller binding is required in %s mode", mode)
}
copyBindings := make(map[string]string, len(bindings))
for system, principal := range bindings {
if strings.TrimSpace(system) == "" || strings.TrimSpace(principal) == "" {
return nil, fmt.Errorf("caller bindings require non-empty system and principal")
}
copyBindings[system] = principal
}
return &Authenticator{mode: mode, reviewer: reviewer, audience: audience, bindings: copyBindings, warnf: warnf}, nil
}
func Disabled() *Authenticator {
authenticator, _ := New(ModeDisabled, nil, "", nil, nil)
return authenticator
}
// Authorize verifies the bearer token and binds every resource.system value to
// the authenticated workload principal. Warn mode records the same failures but
// permits the request so callers can be migrated before enforcement is enabled.
// The returned identity is populated whenever a token was reviewed, including
// warn-mode binding failures, so provenance can name an observed principal.
func (a *Authenticator) Authorize(ctx context.Context, authorization string, systems []string) (Identity, error) {
if a == nil || a.mode == ModeDisabled {
return Identity{}, nil
}
identity, err := a.authorize(ctx, authorization, systems)
if err != nil && a.mode == ModeWarn {
if a.warnf != nil {
a.warnf("caller authentication warning: %v", err)
}
return identity, nil
}
return identity, err
}
// Record returns the provenance object for a reviewed identity. Disabled mode
// is stated as {"mode":"disabled"} with no principal.
func (a *Authenticator) Record(identity Identity) Record {
mode := ModeDisabled
audience := ""
if a != nil {
mode = a.mode
audience = a.audience
}
record := Record{Mode: mode}
if mode == ModeDisabled || strings.TrimSpace(identity.Username) == "" {
return record
}
record.Principal = identity.Username
record.Audience = audience
record.NotAfter = identity.NotAfter
return record
}
func (a *Authenticator) authorize(ctx context.Context, authorization string, systems []string) (Identity, error) {
token, ok := strings.CutPrefix(authorization, "Bearer ")
if !ok || strings.TrimSpace(token) == "" || strings.ContainsAny(strings.TrimSpace(token), " \t\r\n") {
return Identity{}, ErrUnauthenticated
}
token = strings.TrimSpace(token)
identity, err := a.reviewer.Review(ctx, token)
if err != nil {
if errors.Is(err, ErrUnauthenticated) {
return Identity{}, err
}
return Identity{}, fmt.Errorf("%w: %v", ErrUnavailable, err)
}
if identity.NotAfter.IsZero() {
if exp, ok := tokenExpiry(token); ok {
identity.NotAfter = exp
}
}
if strings.TrimSpace(identity.Username) == "" || !contains(identity.Audiences, a.audience) {
return Identity{}, ErrUnauthenticated
}
if len(systems) == 0 {
return identity, fmt.Errorf("%w: request has no resources", ErrForbidden)
}
for _, system := range systems {
expected, found := a.bindings[system]
if !found || expected != identity.Username {
return identity, fmt.Errorf("%w: principal %q cannot represent system %q", ErrForbidden, identity.Username, system)
}
}
return identity, nil
}
func contains(values []string, wanted string) bool {
for _, value := range values {
if value == wanted {
return true
}
}
return false
}