passkey

package
v0.7.0 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: MIT Imports: 10 Imported by: 0

Documentation

Overview

Package passkey adds WebAuthn — passkeys, Touch ID, hardware security keys — to authit, both as a second factor after a password and as a primary credential that replaces one.

The two shapes, and why the difference matters

A passkey used as a *second factor* proves possession of a device; the password was the first factor. A passkey used as the *only* factor has to carry both, and it does that through user verification: the authenticator asks for a PIN or a biometric before it will sign. Possession plus verification is two factors in one gesture — but only if user verification actually happened, which is why Config.UserVerification exists and why VerificationRequired is the default.

Get that wrong and a passkey login is single-factor: anyone holding an unlocked device is the user.

What it does not do

It issues no authit credential. Like pat, device and oidc, it answers "who is this, and how strongly" and leaves what to hand back to the host.

It does not verify attestation against the FIDO Metadata Service. Doing so answers "is this authenticator model one I trust", which matters for enterprise deployments enforcing a hardware policy and is irrelevant to almost everybody else; it needs a metadata blob, its trust chain, and a refresh story. Registration here records the attestation the authenticator supplied, and does not judge it.

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrConfig means the Service was constructed without something it
	// cannot work without, such as an RPID.
	ErrConfig = errors.New("authit/passkey: RPID is required")
	// ErrCeremony means the browser's response did not verify: a bad
	// signature, a mismatched challenge, a wrong origin or relying party,
	// an expired ceremony. It is deliberately one error covering all of
	// them -- which specific check failed is useful in a log and is an
	// oracle in a response body.
	ErrCeremony = errors.New("authit/passkey: credential response did not verify")
	// ErrSession means the stored session data could not be decoded, so
	// the ceremony cannot be completed.
	ErrSession = errors.New("authit/passkey: ceremony session is missing or unreadable")
	// ErrCredentialNotFound means no registered credential matches.
	ErrCredentialNotFound = errors.New("authit/passkey: no such credential")
	// ErrNoCredentials means the user has registered no authenticators, so
	// there is nothing to sign a challenge with.
	ErrNoCredentials = errors.New("authit/passkey: user has no registered credentials")
	// ErrAlreadyRegistered means this authenticator is already registered.
	// Returned rather than silently re-registering, which would leave two
	// rows for one credential and make the assertion lookup ambiguous.
	ErrAlreadyRegistered = errors.New("authit/passkey: this authenticator is already registered")
	// ErrCloneWarning means the assertion's signature counter did not
	// advance, which is evidence the credential's private key exists in
	// more than one place. See CloneAction.
	ErrCloneWarning = errors.New("authit/passkey: signature counter did not advance; the authenticator may be cloned")
	// ErrUserVerificationRequired means the authenticator did not verify
	// the user, and this Service requires it.
	ErrUserVerificationRequired = errors.New("authit/passkey: authenticator did not verify the user")
	// ErrLastCredential means removing this credential would leave the
	// user unable to sign in at all.
	ErrLastCredential = errors.New("authit/passkey: cannot remove the account's only remaining credential")
)

Functions

This section is empty.

Types

type CloneAction

type CloneAction int

CloneAction says what to do when an assertion's signature counter fails to advance.

A counter that does not increase is the specification's one built-in signal that a credential's private key exists in more than one place — that somebody extracted it from a hardware key, or restored a backup of something that was not supposed to be backed up. The specification leaves the response to the relying party.

const (
	// CloneReject refuses the login and flags the credential. This is the
	// default: a counter regression means either a cloned authenticator or
	// a broken one, and neither is something to sign a user in on.
	CloneReject CloneAction = iota
	// CloneFlag records the warning on the credential and allows the
	// login. Choose it only knowing that authenticators do exist which
	// report a counter of zero forever — those never trigger this at all,
	// so a regression from a nonzero counter remains meaningful.
	CloneFlag
)

type Config

type Config struct {
	// RPDisplayName is shown to the user by the authenticator ("Example
	// Inc").
	RPDisplayName string
	// RPID is the relying party identifier: your registrable domain, with
	// no scheme, port or path ("example.com"). A credential is bound to it
	// permanently — change it and every passkey your users hold stops
	// working, with no migration.
	RPID string
	// RPOrigins are the exact origins allowed to complete a ceremony
	// ("https://example.com"). Every entry is checked against the origin
	// the browser reports, which is what stops a page on another site from
	// driving your authenticator.
	RPOrigins []string
	// UserVerification defaults to protocol.VerificationRequired. Lower it
	// only for a passkey used strictly as a second factor behind a
	// password, and understand that doing so makes a passkey-only login
	// single-factor.
	UserVerification protocol.UserVerificationRequirement
	// OnClone defaults to CloneReject.
	OnClone CloneAction
	// Timeout bounds how long a ceremony may take. Defaults to 60s.
	Timeout time.Duration
	// AuditLogger receives registration, login and revocation events.
	AuditLogger audit.Logger
}

Config tunes the package.

type Options

type Options []byte

Options is the JSON to hand the browser. Write it as the response body; the page passes it to navigator.credentials.create() or .get().

It is bytes rather than a struct so that the WebAuthn library's types do not leak into authit's API, and so a host can serve it without a re-encode.

type Result

type Result struct {
	// User is the authit account that just authenticated.
	User store.User
	// Credential is the authenticator that signed, as it now stands.
	Credential store.WebAuthnCredential
	// UserVerified reports whether the authenticator verified the user
	// with a PIN or biometric.
	//
	// It is the difference between one factor and two. A Service
	// configured with VerificationRequired refuses an assertion without
	// it, so this is always true there; it is reported so that a Service
	// configured otherwise can still tell, and can decide to ask for a
	// password as well.
	UserVerified bool
}

Result is a completed authentication. It is not a session: user.Service.IssueSession with user.LoginMethodPasskey turns User.ID into one.

type Service

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

Service runs the WebAuthn ceremonies.

func NewService

func NewService(stores Stores, cfg Config) (*Service, error)

NewService constructs a Service.

func (*Service) BeginDiscoverableLogin

func (s *Service) BeginDiscoverableLogin(ctx context.Context) (Options, Session, error)

BeginDiscoverableLogin starts a usernameless sign-in: the browser offers whatever passkeys it holds for this relying party, and the assertion carries the user handle that identifies the account.

This is the flow that makes a passkey feel like nothing at all — no email typed, no password. Note that it names no user, so it also cannot leak whether a given account exists.

func (*Service) BeginLogin

func (s *Service) BeginLogin(ctx context.Context, userID string) (Options, Session, error)

BeginLogin starts an assertion for a known user — the shape used when the user has already typed an email, or when the passkey is a second factor behind a password.

func (*Service) BeginRegistration

func (s *Service) BeginRegistration(ctx context.Context, userID string) (Options, Session, error)

BeginRegistration starts registering a new authenticator for a user who has already been authenticated by some other means.

The caller must have authenticated the user first. This ceremony proves possession of an authenticator; it proves nothing about whose account it should be attached to, so calling it on an unauthenticated request lets anybody add their own passkey to somebody else's account.

func (*Service) FinishDiscoverableLogin

func (s *Service) FinishDiscoverableLogin(ctx context.Context, sess Session, r *http.Request) (Result, error)

FinishDiscoverableLogin verifies a usernameless assertion and resolves it to the account that owns the credential.

func (*Service) FinishLogin

func (s *Service) FinishLogin(ctx context.Context, userID string, sess Session, r *http.Request) (Result, error)

FinishLogin verifies an assertion for a known user.

func (*Service) FinishRegistration

func (s *Service) FinishRegistration(ctx context.Context, userID, name string, sess Session, r *http.Request) (store.WebAuthnCredential, error)

FinishRegistration verifies the browser's response and stores the new credential. name is a user-facing label.

func (*Service) List

func (s *Service) List(ctx context.Context, userID string) ([]store.WebAuthnCredential, error)

List returns a user's registered authenticators, for a settings page.

func (*Service) Remove

func (s *Service) Remove(ctx context.Context, userID, credentialID string) error

Remove revokes a credential — the button a user presses when a phone is lost.

It refuses to remove the last thing the account can be reached with: a user with no password and no remaining authenticator still exists and nobody, including its owner, can sign in. Deleting the account is a different decision and belongs to the host.

func (*Service) Rename

func (s *Service) Rename(ctx context.Context, userID, credentialID, name string) error

Rename relabels a credential. Somebody with four passkeys needs to be able to tell which one to revoke.

type Session

type Session []byte

Session is the handle to an in-flight ceremony: 32 random bytes, and nothing else.

The host stores it — a short-lived HttpOnly cookie is the usual place — and gives it back to the matching Finish call, which redeems it. The ceremony state itself lives in store.WebAuthnChallengeStore and never leaves the server.

It did not always. This carried the marshalled ceremony state until a security review found what that costs: the challenge is what the signature is verified against, so a caller able to edit it could present an assertion captured earlier and have it checked against expectations of its own choosing. Signing the cookie stops the editing; only redeeming the handle exactly once stops the replay, and only the server can do that.

Treat it as a credential. It is single-use and short-lived, but until it is redeemed it is the ceremony: whoever holds it can finish it.

type Stores

type Stores struct {
	Users       store.UserStore
	Credentials store.WebAuthnCredentialStore
	// Challenges holds in-flight ceremonies. Required.
	//
	// It is not optional the way Tx is, and the difference is deliberate.
	// Leaving Tx nil costs atomicity a host may never have had; leaving
	// this nil would cost single-use challenges, and a challenge that can
	// be redeemed twice is a captured assertion that can be replayed --
	// indefinitely, against any authenticator reporting a signature
	// counter of zero, which is every synced passkey. There is no version
	// of that worth offering as a convenience.
	Challenges store.WebAuthnChallengeStore
	// Tx is optional; see store.TxRunner.
	Tx store.TxRunner
}

Stores are the ports this package needs.

Jump to

Keyboard shortcuts

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