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 ¶
- Variables
- type CloneAction
- type Config
- type Options
- type Result
- type Service
- func (s *Service) BeginDiscoverableLogin(ctx context.Context) (Options, Session, error)
- func (s *Service) BeginLogin(ctx context.Context, userID string) (Options, Session, error)
- func (s *Service) BeginRegistration(ctx context.Context, userID string) (Options, Session, error)
- func (s *Service) FinishDiscoverableLogin(ctx context.Context, sess Session, r *http.Request) (Result, error)
- func (s *Service) FinishLogin(ctx context.Context, userID string, sess Session, r *http.Request) (Result, error)
- func (s *Service) FinishRegistration(ctx context.Context, userID, name string, sess Session, r *http.Request) (store.WebAuthnCredential, error)
- func (s *Service) List(ctx context.Context, userID string) ([]store.WebAuthnCredential, error)
- func (s *Service) Remove(ctx context.Context, userID, credentialID string) error
- func (s *Service) Rename(ctx context.Context, userID, credentialID, name string) error
- type Session
- type Stores
Constants ¶
This section is empty.
Variables ¶
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.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service runs the WebAuthn ceremonies.
func NewService ¶
NewService constructs a Service.
func (*Service) BeginDiscoverableLogin ¶
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 ¶
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 ¶
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) Remove ¶
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.
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.