authkit/models.go
juancwu ca5525d4bd Cap refresh chain lifetime via RefreshChainAbsoluteTTL
Sessions had an absolute cap (created_at + SessionAbsoluteTTL) but the
JWT path only had per-token TTL on the refresh row, letting a
well-behaved client refresh indefinitely. Add chain_started_at to
authkit_tokens, copy it forward on every rotation, and reject in
RefreshJWT when now > chainStartedAt + RefreshChainAbsoluteTTL.
Default 30d, mirroring SessionAbsoluteTTL.

Schema, verifier, queries, model, and integration test updated.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-26 23:41:02 +00:00

114 lines
3.1 KiB
Go

package authkit
import (
"net/netip"
"time"
"github.com/google/uuid"
)
// User is the canonical account record. Password hash is empty (and stored
// NULL in the DB) when no credential has been set — accounts created via
// invite or magic-link-only flows live in this state until SetPassword runs.
type User struct {
ID uuid.UUID
Email string
EmailNormalized string
EmailVerifiedAt *time.Time
PasswordHash string
SessionVersion int
LastLoginAt *time.Time
CreatedAt time.Time
UpdatedAt time.Time
}
// Session is an opaque server-side credential bound to one user.
type Session struct {
IDHash []byte
UserID uuid.UUID
UserAgent string
IP netip.Addr
CreatedAt time.Time
LastSeenAt time.Time
ExpiresAt time.Time
}
// TokenKind enumerates the single-use credentials persisted in authkit_tokens.
type TokenKind string
const (
TokenEmailVerify TokenKind = "email_verify"
TokenPasswordReset TokenKind = "password_reset"
TokenMagicLink TokenKind = "magic_link"
TokenEmailOTP TokenKind = "email_otp"
TokenRefresh TokenKind = "refresh"
)
// Token is one row in authkit_tokens. AttemptsRemaining is non-nil only for
// tokens that allow retry on incorrect input (email OTPs); other kinds are
// strictly one-shot via ConsumeToken. ChainStartedAt is non-nil only for
// refresh-token rows; copied forward on every rotation so the absolute-cap
// check in RefreshJWT is O(1).
type Token struct {
Hash []byte
Kind TokenKind
UserID uuid.UUID
ChainID *string
ChainStartedAt *time.Time
ConsumedAt *time.Time
AttemptsRemaining *int
CreatedAt time.Time
ExpiresAt time.Time
}
// ServiceKey is a machine credential. It carries no identity — service tokens
// are produced by applications for outbound API access or inbound automation,
// and authorize via Abilities resolved through the join table.
type ServiceKey struct {
IDHash []byte
Name string
Abilities []string
LastUsedAt *time.Time
CreatedAt time.Time
ExpiresAt *time.Time
RevokedAt *time.Time
}
// HasAbility reports whether the service key carries the named ability slug.
func (k *ServiceKey) HasAbility(slug string) bool {
for _, a := range k.Abilities {
if a == slug {
return true
}
}
return false
}
// Role groups permissions for assignment to users. Slug is the immutable
// business key; Label is an optional human-readable name.
type Role struct {
ID uuid.UUID
Slug string
Label string
CreatedAt time.Time
}
// Permission is a unit of authorization. Granted to users either through a
// role or directly via authkit_user_permissions.
type Permission struct {
ID uuid.UUID
Slug string
Label string
CreatedAt time.Time
}
// Ability is a unit of authorization for service tokens. Abilities are a
// separate vocabulary from Permissions because they target machines, not
// users — keep them distinct so middleware predicates remain clear about
// which subject they're authorizing.
type Ability struct {
ID uuid.UUID
Slug string
Label string
CreatedAt time.Time
}