Add owner-agnostic service tokens
Introduces ServiceKey, a parallel primitive to APIKey for server-to-server auth where the owner is not an authkit user (e.g. an application or tenant row the consumer manages). owner_id has no FK and no RBAC linkage; cascade on owner-delete is the consumer's responsibility. AuthenticateServiceKey returns *ServiceKey directly rather than *Principal since service tokens have no user. Also exports MintOpaqueSecret / HashOpaqueSecret / ParseOpaqueSecret so both API-key and service-key code share one mint/parse implementation instead of duplicating it. Deps.ServiceKeys is required (panics in New if nil) — existing call sites must add ServiceKeys: stores.ServiceKeys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
9aae7b1c12
commit
4942e4dbdc
16 changed files with 664 additions and 24 deletions
46
tokens.go
46
tokens.go
|
|
@ -19,16 +19,19 @@ const (
|
|||
prefixSession = "sess"
|
||||
prefixRefresh = "rfr"
|
||||
prefixAPIKey = "ak"
|
||||
prefixServiceKey = "sk"
|
||||
prefixEmailVerify = "evr"
|
||||
prefixPasswordRset = "pwr"
|
||||
prefixMagicLink = "mlnk"
|
||||
)
|
||||
|
||||
// mintSecret generates a new opaque secret of the given prefix kind and
|
||||
// returns the plaintext (to be returned to the user, never stored) and the
|
||||
// SHA-256 lookup hash (to be stored).
|
||||
func mintSecret(prefix string, rng io.Reader) (plaintext string, hash []byte, err error) {
|
||||
const op = "authkit.mintSecret"
|
||||
// MintOpaqueSecret generates a fresh opaque secret with the given prefix.
|
||||
// Returns the plaintext (show once, never persist) and the SHA-256 lookup
|
||||
// hash. A nil rng falls back to crypto/rand.Reader. Exposed so consumers
|
||||
// building bespoke storage can produce secrets in the same shape authkit
|
||||
// uses internally.
|
||||
func MintOpaqueSecret(rng io.Reader, prefix string) (plaintext string, hash []byte, err error) {
|
||||
const op = "authkit.MintOpaqueSecret"
|
||||
if rng == nil {
|
||||
rng = rand.Reader
|
||||
}
|
||||
|
|
@ -38,27 +41,40 @@ func mintSecret(prefix string, rng io.Reader) (plaintext string, hash []byte, er
|
|||
}
|
||||
body := base64.RawURLEncoding.EncodeToString(buf)
|
||||
plaintext = prefix + "_" + body
|
||||
hash = hashSecret(plaintext)
|
||||
hash = HashOpaqueSecret(plaintext)
|
||||
return plaintext, hash, nil
|
||||
}
|
||||
|
||||
// hashSecret returns sha256(plaintext) — the lookup key for opaque secrets.
|
||||
// Plaintexts have full entropy from a CSPRNG so a plain hash is sufficient
|
||||
// (no per-record salt needed; the random body is the salt).
|
||||
func hashSecret(plaintext string) []byte {
|
||||
// HashOpaqueSecret returns sha256(plaintext) — the lookup key for opaque
|
||||
// secrets. Plaintexts have full entropy from a CSPRNG so a plain hash is
|
||||
// sufficient (no per-record salt needed; the random body is the salt).
|
||||
func HashOpaqueSecret(plaintext string) []byte {
|
||||
sum := sha256.Sum256([]byte(plaintext))
|
||||
return sum[:]
|
||||
}
|
||||
|
||||
// parseSecret validates that a plaintext starts with the expected prefix
|
||||
// and returns the lookup hash. The prefix check is constant-time relative
|
||||
// to a fixed-length comparison.
|
||||
func parseSecret(prefix, plaintext string) (hash []byte, ok bool) {
|
||||
// ParseOpaqueSecret validates that a plaintext begins with the expected
|
||||
// prefix and returns the lookup hash. Returns ok=false on prefix mismatch.
|
||||
func ParseOpaqueSecret(prefix, plaintext string) (hash []byte, ok bool) {
|
||||
want := prefix + "_"
|
||||
if !strings.HasPrefix(plaintext, want) {
|
||||
return nil, false
|
||||
}
|
||||
return hashSecret(plaintext), true
|
||||
return HashOpaqueSecret(plaintext), true
|
||||
}
|
||||
|
||||
// mintSecret is the internal entry point; existing callers pass prefix
|
||||
// first to match call-site readability ("mint a token of kind X").
|
||||
func mintSecret(prefix string, rng io.Reader) (plaintext string, hash []byte, err error) {
|
||||
return MintOpaqueSecret(rng, prefix)
|
||||
}
|
||||
|
||||
func hashSecret(plaintext string) []byte {
|
||||
return HashOpaqueSecret(plaintext)
|
||||
}
|
||||
|
||||
func parseSecret(prefix, plaintext string) (hash []byte, ok bool) {
|
||||
return ParseOpaqueSecret(prefix, plaintext)
|
||||
}
|
||||
|
||||
// constantTimeEqual is a thin wrapper for readability at call sites.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue