add token type with permission checks and composable matchers

This commit is contained in:
juancwu 2026-04-29 01:58:46 +00:00
commit 8b5fcc87c3
4 changed files with 499 additions and 0 deletions

114
token.go Normal file
View file

@ -0,0 +1,114 @@
package ficha
import (
"encoding/json"
"time"
)
// Token is a validated, decrypted token. It exposes the consumer-defined
// permissions and data, and provides methods for permission checks.
//
// Tokens are returned by Issuer.Validate. Consumers do not construct them
// directly. A Token is read-only — its methods do not mutate state.
type Token struct {
id string
issuedAt time.Time
expiresAt time.Time
permissions []string
data json.RawMessage
}
// newToken builds a Token from a decoded payload. Package-private —
// only the Issuer constructs Tokens, after successful validation.
func newToken(p payload) *Token {
return &Token{
id: p.ID,
issuedAt: time.Unix(p.Iat, 0),
expiresAt: time.Unix(p.Exp, 0),
permissions: p.Permissions,
data: p.Data,
}
}
// ID returns the unique token identifier (used by the revocation store).
func (t *Token) ID() string { return t.id }
// IssuedAt returns when the token was issued.
func (t *Token) IssuedAt() time.Time { return t.issuedAt }
// ExpiresAt returns when the token expires.
func (t *Token) ExpiresAt() time.Time { return t.expiresAt }
// Permissions returns a copy of the token's permission strings.
// The returned slice is safe to retain and modify.
func (t *Token) Permissions() []string {
out := make([]string, len(t.permissions))
copy(out, t.permissions)
return out
}
// UnmarshalData decodes the consumer-defined data blob into v.
// Returns nil with v unchanged if the token has no data.
func (t *Token) UnmarshalData(v any) error {
if len(t.data) == 0 {
return nil
}
return json.Unmarshal(t.data, v)
}
// Has reports whether the token holds the given permission (exact match).
func (t *Token) Has(perm string) bool {
for _, p := range t.permissions {
if p == perm {
return true
}
}
return false
}
// HasAll reports whether the token holds every given permission.
// Returns true when called with no arguments (vacuous truth). For a
// fail-closed variant that returns false on empty input, see RequiresAll.
func (t *Token) HasAll(perms ...string) bool {
for _, p := range perms {
if !t.Has(p) {
return false
}
}
return true
}
// RequiresAll reports whether the token holds every given permission,
// and requires that at least one permission be specified. Returns false
// on empty input — use this when an empty permission list indicates a
// programmer error rather than a public/no-auth route.
//
// For the vacuous-truth variant (empty input → true), use HasAll.
func (t *Token) RequiresAll(perms ...string) bool {
if len(perms) == 0 {
return false
}
return t.HasAll(perms...)
}
// HasAny reports whether the token holds at least one of the given permissions.
// Returns false when called with no arguments.
func (t *Token) HasAny(perms ...string) bool {
for _, p := range perms {
if t.Has(p) {
return true
}
}
return false
}
// HasNone reports whether the token holds none of the given permissions.
// Returns true when called with no arguments.
func (t *Token) HasNone(perms ...string) bool {
for _, p := range perms {
if t.Has(p) {
return false
}
}
return true
}