Files
gamertan bfe6cfd29e
verify / verify (push) Successful in 3m40s
auth: publish passkey foundations preview
2026-08-21 17:33:00 -04:00

114 lines
3.7 KiB
Go

// SPDX-License-Identifier: MPL-2.0
// Package authwebauthn provides storage-neutral, passkey-only WebAuthn
// ceremonies. It owns relying-party policy, bounded single-use ceremony state,
// credential lifecycle, and recovery tokens while delegating protocol parsing
// and signature verification to a pinned WebAuthn implementation.
package authwebauthn
import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"time"
"gamertan.com/web/auth"
)
var (
ErrCeremonyNotFound = errors.New("authwebauthn: ceremony not found")
ErrCredentialNotFound = errors.New("authwebauthn: credential not found")
ErrEnrollmentNotFound = errors.New("authwebauthn: enrollment token not found")
ErrCredentialFloor = errors.New("authwebauthn: the required credential floor cannot be crossed")
ErrLastCredential = errors.New("authwebauthn: the last credential cannot be removed remotely")
ErrOperationBinding = errors.New("authwebauthn: operation binding does not match")
ErrPasskeyReadiness = errors.New("authwebauthn: at least two passkeys are required")
ErrUnsupportedCredential = errors.New("authwebauthn: credential algorithm is unsupported")
)
const (
CeremonyRegistration = "registration"
CeremonyLogin = "login"
CeremonyApproval = "approval"
)
type Credential struct {
ID []byte
UserID string
Label string
Data json.RawMessage
CreatedAt time.Time
LastUsedAt time.Time
}
// CredentialSummary is the non-secret credential metadata applications may
// show to an authenticated account owner. It intentionally excludes the
// stored public-key document and user identifier.
type CredentialSummary struct {
ID []byte
Label string
CreatedAt time.Time
LastUsedAt time.Time
}
type EnrollmentToken struct {
Digest [32]byte
UserID string
CreatedAt time.Time
ExpiresAt time.Time
}
type Ceremony struct {
Digest [32]byte
Kind string
UserID string
Label string
SessionData json.RawMessage
BindingDigest [32]byte
CreatedAt time.Time
ExpiresAt time.Time
}
type BeginResult struct {
CeremonyToken string `json:"ceremony_token"`
PublicKey json.RawMessage `json:"public_key"`
ExpiresAt time.Time `json:"expires_at"`
}
type Authentication struct {
SessionToken string
Principal auth.Principal
CredentialID []byte
CloneWarning bool
}
type Approval struct {
User auth.User
CredentialID []byte
BindingDigest [32]byte
CloneWarning bool
ApprovedAt time.Time
}
// Repository persists passkey-specific state. Implementations must consume
// enrollment tokens and ceremonies atomically and must perform recovery and
// credential removal invariants in transactions.
type Repository interface {
CreatePasskeyUser(context.Context, auth.User, EnrollmentToken, auth.AuditEvent) error
UserByID(context.Context, string) (auth.User, error)
UserByIdentifier(context.Context, string) (auth.User, error)
UserByCredentialID(context.Context, []byte) (auth.User, error)
CredentialsByUserID(context.Context, string) ([]Credential, error)
SaveCredential(context.Context, Credential, auth.AuditEvent) error
UpdateCredential(context.Context, Credential) error
DeleteCredential(context.Context, string, []byte, int, auth.AuditEvent) error
CredentialCount(context.Context, string) (int, error)
CreateCeremony(context.Context, Ceremony) error
TakeCeremony(context.Context, [32]byte, time.Time) (Ceremony, error)
ConsumeEnrollmentToken(context.Context, [32]byte, time.Time) (auth.User, error)
RecoverUser(context.Context, string, EnrollmentToken, auth.AuditEvent) (auth.User, error)
}
func BindingDigest(value []byte) [32]byte { return sha256.Sum256(value) }