- migration 0012: workers.owner_id + trust_level (trusted/untrusted) - verifier/authctx read the JWT verified claim; IsTrusted() = admin||verified - /workers/register resolves trust from auth: service token or verified/admin JWT -> trusted; plain user JWT -> untrusted, tagged with owner_id - claim quarantines untrusted workers (no tasks) until quorum (C2) lands - unit tests for trust resolution, quarantine, and the verified claim Additive and backward compatible: shared-token workers stay trusted, so the existing worker flow and team tests are unchanged. Quorum verification (C2) is deferred.
61 lines
1.8 KiB
Go
61 lines
1.8 KiB
Go
// Package token verifies the HS256 JWTs minted by the userservice. The
|
|
// coordinator only ever *verifies* — it never issues — so this is a deliberately
|
|
// small counterpart to the userservice's issuer. Verification is local: the
|
|
// shared secret is enough, with no runtime call back to the userservice.
|
|
package token
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// Claims is the subset of a userservice token the coordinator cares about.
|
|
type Claims struct {
|
|
UserID uuid.UUID
|
|
Role string
|
|
Verified bool
|
|
}
|
|
|
|
// Verifier checks tokens against the shared HS256 secret.
|
|
type Verifier struct {
|
|
secret []byte
|
|
}
|
|
|
|
// NewVerifier returns a Verifier, or nil when secret is empty — a nil Verifier
|
|
// means user-JWT auth is disabled and only the shared service token is accepted.
|
|
func NewVerifier(secret string) *Verifier {
|
|
if secret == "" {
|
|
return nil
|
|
}
|
|
return &Verifier{secret: []byte(secret)}
|
|
}
|
|
|
|
type claims struct {
|
|
Role string `json:"role"`
|
|
Verified bool `json:"verified"`
|
|
jwt.RegisteredClaims
|
|
}
|
|
|
|
// Verify checks the signature and expiry and returns the identity. It pins the
|
|
// algorithm to HMAC, rejecting a token that asks for "none" or an RS256 public
|
|
// key — the classic algorithm-substitution attack.
|
|
func (v *Verifier) Verify(raw string) (Claims, error) {
|
|
var c claims
|
|
_, err := jwt.ParseWithClaims(raw, &c, func(t *jwt.Token) (any, error) {
|
|
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
|
|
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
|
|
}
|
|
return v.secret, nil
|
|
})
|
|
if err != nil {
|
|
return Claims{}, err
|
|
}
|
|
id, err := uuid.Parse(c.Subject)
|
|
if err != nil {
|
|
return Claims{}, fmt.Errorf("token subject is not a uuid: %w", err)
|
|
}
|
|
return Claims{UserID: id, Role: c.Role, Verified: c.Verified}, nil
|
|
}
|