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
92
service_service_key.go
Normal file
92
service_service_key.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package authkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.juancwu.dev/juancwu/errx"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// IssueServiceKey mints a fresh owner-agnostic service token. ownerKind is a
|
||||
// consumer-defined namespace label (e.g. "application", "tenant") and ownerID
|
||||
// is the owning entity's id; authkit makes no assumption about either. The
|
||||
// plaintext is returned (show-once) and the SHA-256 lookup hash is stored.
|
||||
// Pass ttl=nil for a non-expiring key.
|
||||
func (a *Auth) IssueServiceKey(ctx context.Context, ownerKind string, ownerID uuid.UUID, name string, abilities []string, ttl *time.Duration) (string, *ServiceKey, error) {
|
||||
const op = "authkit.Auth.IssueServiceKey"
|
||||
plaintext, hash, err := MintOpaqueSecret(a.cfg.Random, prefixServiceKey)
|
||||
if err != nil {
|
||||
return "", nil, errx.Wrap(op, err)
|
||||
}
|
||||
now := a.now()
|
||||
k := &ServiceKey{
|
||||
IDHash: hash,
|
||||
OwnerID: ownerID,
|
||||
OwnerKind: ownerKind,
|
||||
Name: name,
|
||||
Abilities: append([]string(nil), abilities...),
|
||||
CreatedAt: now,
|
||||
}
|
||||
if ttl != nil {
|
||||
exp := now.Add(*ttl)
|
||||
k.ExpiresAt = &exp
|
||||
}
|
||||
if err := a.deps.ServiceKeys.CreateServiceKey(ctx, k); err != nil {
|
||||
return "", nil, errx.Wrap(op, err)
|
||||
}
|
||||
return plaintext, k, nil
|
||||
}
|
||||
|
||||
// AuthenticateServiceKey validates a service token, touches last_used_at
|
||||
// (best-effort), and returns the stored *ServiceKey. Unlike API keys, no
|
||||
// Principal is returned — service tokens have no owning user, so the
|
||||
// Principal abstraction does not fit. Consumers needing a Principal can
|
||||
// build one from the returned key.
|
||||
func (a *Auth) AuthenticateServiceKey(ctx context.Context, plaintext string) (*ServiceKey, error) {
|
||||
const op = "authkit.Auth.AuthenticateServiceKey"
|
||||
hash, ok := ParseOpaqueSecret(prefixServiceKey, plaintext)
|
||||
if !ok {
|
||||
return nil, errx.Wrap(op, ErrServiceKeyInvalid)
|
||||
}
|
||||
k, err := a.deps.ServiceKeys.GetServiceKey(ctx, hash)
|
||||
if err != nil {
|
||||
return nil, errx.Wrap(op, err)
|
||||
}
|
||||
now := a.now()
|
||||
if k.RevokedAt != nil {
|
||||
return nil, errx.Wrap(op, ErrServiceKeyInvalid)
|
||||
}
|
||||
if k.ExpiresAt != nil && !k.ExpiresAt.After(now) {
|
||||
return nil, errx.Wrap(op, ErrServiceKeyInvalid)
|
||||
}
|
||||
_ = a.deps.ServiceKeys.TouchServiceKey(ctx, hash, now)
|
||||
out := *k
|
||||
out.Abilities = append([]string(nil), k.Abilities...)
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
// RevokeServiceKey marks a service token revoked. Idempotent on
|
||||
// already-revoked keys.
|
||||
func (a *Auth) RevokeServiceKey(ctx context.Context, plaintext string) error {
|
||||
const op = "authkit.Auth.RevokeServiceKey"
|
||||
hash, ok := ParseOpaqueSecret(prefixServiceKey, plaintext)
|
||||
if !ok {
|
||||
return errx.Wrap(op, ErrServiceKeyInvalid)
|
||||
}
|
||||
if err := a.deps.ServiceKeys.RevokeServiceKey(ctx, hash, a.now()); err != nil {
|
||||
return errx.Wrap(op, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListServiceKeys returns every service token issued for the given
|
||||
// (ownerKind, ownerID) pair, including revoked and expired keys.
|
||||
func (a *Auth) ListServiceKeys(ctx context.Context, ownerKind string, ownerID uuid.UUID) ([]*ServiceKey, error) {
|
||||
const op = "authkit.Auth.ListServiceKeys"
|
||||
out, err := a.deps.ServiceKeys.ListServiceKeysByOwner(ctx, ownerKind, ownerID)
|
||||
if err != nil {
|
||||
return nil, errx.Wrap(op, err)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue