Help us shape the future of Go! Take the 2026 Go Developer Survey

passkeys

package
v0.40.22 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 2, 2026 License: MIT Imports: 11 Imported by: 0

Documentation

Overview

Package passkeys is Rivolt's WebAuthn: passkeys as a second way to sign in to an email account, next to the password (Sprint 10; DA-approved design). The server runs the ceremonies (github.com/go-webauthn/webauthn) and keeps only public values: a credential id, a public key, a counter and a few flags. Kratos is not involved: a good assertion resolves to a Rivolt user, and the caller accepts the Hydra login exactly as the password path does.

Rules, from DA: the relying party is the configured RP ID with its origins matched exactly; attestation is "none" (no FIDO metadata service, no outbound call from the library); user verification is required on both ceremonies and checked in the authenticator data, not only requested; discoverable (resident) credentials are required so sign-in needs no username; a ceremony is single use and expires in five minutes; a signature counter that goes backwards is refused. Nothing it logs carries a credential id, public key, challenge or user handle.

Index

Constants

View Source
const (
	ReasonCeremony     = "ceremony" // unknown, expired, used or someone else's
	ReasonUnknown      = "unknown_credential"
	ReasonAssertion    = "assertion" // the signature, origin, RP or challenge did not check out
	ReasonNoUV         = "no_user_verification"
	ReasonSignCount    = "sign_count"
	ReasonRegistration = "registration"
)

Reasons.

View Source
const CeremonyTTL = 5 * time.Minute

CeremonyTTL is how long a registration or sign-in may take.

View Source
const MaxPerAccount = 10

MaxPerAccount is the most passkeys one account may hold.

Variables

View Source
var ErrTooMany = errors.New("passkeys: too many passkeys on this account")

ErrTooMany is a registration refused because the account already holds MaxPerAccount passkeys.

Functions

func NewHandle

func NewHandle() []byte

newHandle is a fresh random user handle.

Types

type Ceremony

type Ceremony struct {
	ID             uuid.UUID
	Purpose        string // "register" or "login"
	UserID         uuid.UUID
	HydraChallenge string
	Session        []byte // webauthn.SessionData as JSON
	ExpiresAt      time.Time
}

Ceremony is the stored state of a registration or sign-in in progress.

type Config

type Config struct {
	RPID    string   // e.g. "rivolt.dev"
	RPName  string   // shown by the authenticator; default "Rivolt"
	Origins []string // exact origins, e.g. "https://rivolt.dev"
}

Config is the relying party. Disabled (the zero value) unless RPID and Origins are set.

func ConfigFromEnv

func ConfigFromEnv(get func(string) string) Config

ConfigFromEnv reads RIVOLT_WEBAUTHN_RP_ID, RIVOLT_WEBAUTHN_RP_NAME and RIVOLT_WEBAUTHN_ORIGINS (comma separated).

func (Config) Enabled

func (c Config) Enabled() bool

Enabled reports whether passkeys are configured.

type Credential

type Credential struct {
	ID             uuid.UUID
	UserID         uuid.UUID
	CredentialID   []byte
	PublicKey      []byte
	SignCount      uint32
	Transports     []string
	AAGUID         []byte
	BackupEligible bool
	BackupState    bool
	Label          string
	CreatedAt      time.Time
	LastUsedAt     *time.Time
}

Credential is one passkey as the server keeps it.

type PGStore

type PGStore struct {
	// contains filtered or unexported fields
}

PGStore keeps passkeys in Postgres. It works on the system pool: sign-in runs before there is a user in the request, and the tables carry tenant isolation for the per-user pool like every table with a user_id.

func NewPGStore

func NewPGStore(db *sql.DB) *PGStore

NewPGStore builds the store.

func (*PGStore) Add

func (s *PGStore) Add(ctx context.Context, c Credential) error

func (*PGStore) CredentialByID

func (s *PGStore) CredentialByID(ctx context.Context, credentialID []byte) (Credential, bool, error)

func (*PGStore) Credentials

func (s *PGStore) Credentials(ctx context.Context, uid uuid.UUID) ([]Credential, error)

func (*PGStore) Delete

func (s *PGStore) Delete(ctx context.Context, uid, id uuid.UUID) (bool, error)

func (*PGStore) PurgeCeremonies

func (s *PGStore) PurgeCeremonies(ctx context.Context, now time.Time) (int64, error)

func (*PGStore) RecordUse

func (s *PGStore) RecordUse(ctx context.Context, credentialID []byte, signCount uint32, backupState bool, at time.Time) error

func (*PGStore) Rename

func (s *PGStore) Rename(ctx context.Context, uid, id uuid.UUID, label string) (bool, error)

func (*PGStore) SaveCeremony

func (s *PGStore) SaveCeremony(ctx context.Context, c Ceremony) error

func (*PGStore) TakeCeremony

func (s *PGStore) TakeCeremony(ctx context.Context, id uuid.UUID, purpose string, now time.Time) (Ceremony, bool, error)

func (*PGStore) UserByHandle

func (s *PGStore) UserByHandle(ctx context.Context, handle []byte) (uuid.UUID, bool, error)

func (*PGStore) UserHandle

func (s *PGStore) UserHandle(ctx context.Context, uid uuid.UUID) ([]byte, error)

type Refused

type Refused struct{ Reason string }

Refusals. Each carries a fixed reason, safe as a metric label; to the person they all read alike.

func (Refused) Error

func (r Refused) Error() string

type Service

type Service struct {
	// contains filtered or unexported fields
}

Service runs the ceremonies.

func New

func New(cfg Config, st Store) (*Service, error)

New builds the service for cfg.

func (*Service) BeginLogin

func (s *Service) BeginLogin(ctx context.Context, hydraChallenge string) (uuid.UUID, *protocol.CredentialAssertion, error)

BeginLogin starts a usernameless sign-in bound to a Hydra login challenge.

func (*Service) BeginRegistration

func (s *Service) BeginRegistration(ctx context.Context, uid uuid.UUID, name, display string) (uuid.UUID, *protocol.CredentialCreation, error)

BeginRegistration starts adding a passkey to the account uid; name is the login (email) shown by the authenticator.

func (*Service) FinishLogin

func (s *Service) FinishLogin(ctx context.Context, ceremony uuid.UUID, hydraChallenge string, body []byte) (uuid.UUID, Credential, error)

FinishLogin verifies the assertion in body for the ceremony and returns the account and the passkey used. Every failure is a Refused with a fixed reason; the caller answers them all alike.

func (*Service) FinishRegistration

func (s *Service) FinishRegistration(ctx context.Context, uid uuid.UUID, ceremony uuid.UUID, body []byte, label, name, display string) (Credential, error)

FinishRegistration completes it: body is the browser's PublicKeyCredential JSON. Returns the stored passkey.

func (*Service) SetClock

func (s *Service) SetClock(now func() time.Time)

SetClock replaces the clock (tests).

func (*Service) Store

func (s *Service) Store() Store

Store returns the persistence (for the list, rename and delete routes).

type Store

type Store interface {
	// UserHandle returns the account's WebAuthn user handle, creating a random one the first time.
	UserHandle(ctx context.Context, uid uuid.UUID) ([]byte, error)
	// UserByHandle finds the account a handle belongs to.
	UserByHandle(ctx context.Context, handle []byte) (uuid.UUID, bool, error)
	Credentials(ctx context.Context, uid uuid.UUID) ([]Credential, error)
	// CredentialByID finds a credential by the authenticator's credential id.
	CredentialByID(ctx context.Context, credentialID []byte) (Credential, bool, error)
	Add(ctx context.Context, c Credential) error
	// RecordUse stores the new signature counter and backup state and the time of use.
	RecordUse(ctx context.Context, credentialID []byte, signCount uint32, backupState bool, at time.Time) error
	Rename(ctx context.Context, uid, id uuid.UUID, label string) (bool, error)
	Delete(ctx context.Context, uid, id uuid.UUID) (bool, error)
	SaveCeremony(ctx context.Context, c Ceremony) error
	// TakeCeremony returns an unexpired ceremony of the purpose and deletes it, so it can be used once.
	TakeCeremony(ctx context.Context, id uuid.UUID, purpose string, now time.Time) (Ceremony, bool, error)
	PurgeCeremonies(ctx context.Context, now time.Time) (int64, error)
}

Store is the passkeys' persistence (a Postgres store, or a fake in tests).

Directories

Path Synopsis
Package passkeytest is a software authenticator for tests: enough of one to make attestations ("none") and assertions that the WebAuthn library accepts, with the flags, origin and counter under the test's control.
Package passkeytest is a software authenticator for tests: enough of one to make attestations ("none") and assertions that the WebAuthn library accepts, with the flags, origin and counter under the test's control.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL