Documentation ¶
Overview ¶
Package passkey implements the server (Relying Party) side of WebAuthn for discoverable credentials (passkeys).
The package is built around passkey records: opaque strings, handled like prefixed password hashes, that encode everything the server needs to store about a credential.
$webauthn$v=1$transports=hybrid+internal$<base64 authenticator data>
Records are immutable: they are produced by RelyingParty.Register and never change afterwards. Mutable authenticator state (like backup state and User Verification) is not tracked; per-login values can be read from the assertion response with ResponseBackedUp and ResponseUserVerified.
Storage model ¶
Applications are expected to store passkey records in a table keyed only by user ID, for example
CREATE TABLE passkeys (
user_id TEXT NOT NULL, -- indexed, not unique
record TEXT NOT NULL
);
with no index on credential IDs and no uniqueness constraints. No other columns are needed by this package; applications may add their own (e.g. a nickname, creation time, or last-used time for a passkey management UI). Lookups always resolve the user first (from the session during registration, from Inspect during login) and then pass all of the user's records to RelyingParty.Login, which selects the right one.
(Because credentials are never resolved by credential ID across accounts, a credential ID registered maliciously into one account can never affect authentication for another, and the cross-account uniqueness check recommended by the WebAuthn specification becomes unnecessary.)
User IDs ¶
User IDs are opaque strings, at most 64 bytes of printable ASCII. They are stored inside the authenticator and returned in every login, so they MUST NOT contain personal information such as usernames or email addresses, and they cannot be changed later. Generate them with crypto/rand.Text at account creation and map them to accounts in the database. Do not reuse internal account identifiers.
Challenges ¶
RelyingParty.NewLogin and RelyingParty.NewLoginForUser return an opaque request value that must be presented back to RelyingParty.Login.
The application must:
- store the request, either server-side (in memory or in a KV store, keyed by RequestID) or client-side in a cookie;
- delete it upon use, so that each request is accepted at most once; and
- protect its integrity if stored client-side (e.g. with an authenticated cookie), as a forged request could defeat challenge freshness.
Requests expire after Options.Timeout, which the application can also use as the lifetime of stored requests (e.g. as the KV TTL or cookie Max-Age).
Request values don't need to be kept secret: everything they contain is also sent to the client as part of the ceremony options.
(Registration has no request value: with attestation "none", no part of a registration response is signed by a party the server trusts, so a registration challenge can't prove freshness and is not verified. Applications tie a registration to the signed-in session, and must protect the registration endpoint with their usual session and cross-site request forgery defenses, like any other authenticated endpoint.)
Index ¶
- Variables
- func AAGUID(passkey string) ([16]byte, error)
- func BackedUp(passkey string) (bool, error)
- func CredentialID(passkey string) ([]byte, error)
- func Inspect(responseJSON []byte) (requestID, userID string, err error)
- func RequestID(request []byte) string
- func ResponseBackedUp(responseJSON []byte) (bool, error)
- func ResponseUserVerified(responseJSON []byte) (bool, error)
- func Transports(passkey string) ([]string, error)
- type Options
- type RelyingParty
- func (rp *RelyingParty) Login(request, responseJSON []byte, passkeys []string) (matched int, err error)
- func (rp *RelyingParty) NewLogin() (request, optionsJSON []byte, err error)
- func (rp *RelyingParty) NewLoginForUser(userID string, passkeys []string) (request, optionsJSON []byte, err error)
- func (rp *RelyingParty) NewRegistration(user User, passkeys []string) (optionsJSON []byte, err error)
- func (rp *RelyingParty) Register(responseJSON []byte) (passkey string, err error)
- type User
Constants ¶
This section is empty.
Variables ¶
var ErrRequestExpired error
ErrRequestExpired is returned by RelyingParty.Login when the response is valid but the request was created more than Options.Timeout ago. Applications should discard the request and retry the ceremony with a fresh one.
var ErrUnknownCredential error
ErrUnknownCredential is returned by RelyingParty.Login when the response asserts a credential that is not among the provided passkey records. It can be surfaced to the user as "this passkey was removed from the account".
var ErrUserVerificationRequired error
ErrUserVerificationRequired is returned by RelyingParty.Login when the response is valid but user verification was not performed and Options.RequireUserVerification is set. It can be surfaced to the user as "this passkey doesn't support the verification required by this account", with the option to use a different passkey or register this one anew. The login has still failed: applications that want to accept such logins with reduced trust should leave RequireUserVerification unset and check ResponseUserVerified instead.
Functions ¶
func AAGUID ¶
AAGUID returns the authenticator's AAGUID from a passkey record, which may identify the passkey provider (e.g. for display purposes, using the community-maintained AAGUID lists). It is all zeroes if the authenticator did not provide one.
func BackedUp ¶
BackedUp reports whether the credential was backed up (e.g. synced to a cloud account) at registration time. For the current state, check login responses with ResponseBackedUp.
func CredentialID ¶
CredentialID returns the credential ID of a passkey record.
It is not needed for the flows implemented by this package, and is provided for interoperability: exporting credentials, importing them into systems that look credentials up by ID, and building UIs that need a stable per-credential identifier.
func Inspect ¶
Inspect extracts the request ID and the asserted user ID from a login response, without verifying it.
It is meant to be used when a login response is received, to fetch (and delete) the stored request and to load the user's passkey records, before verifying the response with RelyingParty.Login.
The returned values are attacker-controlled until Login succeeds: they must not be used for anything but the lookups above.
userID may be empty for responses to RelyingParty.NewLoginForUser ceremonies, where the application already knows the user.
func RequestID ¶
RequestID returns a unique identifier for a request returned by RelyingParty.NewLogin or RelyingParty.NewLoginForUser, to be used as a storage key. The same value is returned by Inspect for the corresponding response.
func ResponseBackedUp ¶
ResponseBackedUp reports whether the credential asserts it is currently backed up, according to a login response. It can be checked after a successful RelyingParty.Login to inform account recovery decisions.
func ResponseUserVerified ¶
ResponseUserVerified reports whether user verification was performed, according to a login response. It can be checked after a successful RelyingParty.Login to gate sensitive operations.
func Transports ¶
Transports returns the transport hints recorded at registration (e.g. "internal", "hybrid", "usb"). They are used to populate the credential descriptors sent to clients by RelyingParty.NewRegistration and RelyingParty.NewLoginForUser, where they help the client reach the right authenticator during login; they are exposed for interoperability and diagnostics.
Types ¶
type Options ¶
type Options struct {
// Origins is the set of origins from which registrations and logins
// are accepted.
//
// If nil, it defaults to "https://" followed by the RP ID, plus all
// its "https://" subdomains, with no port. If non-nil, it replaces
// the default entirely: deployments that serve sign-in from a
// dedicated origin (e.g. https://accounts.example.com) should list
// exactly that, shedding the rest of the domain.
//
// Each entry is either an exact origin ("https://accounts.example.com",
// "https://example.com:8443", "http://localhost:8080",
// "android:apk-key-hash:...") or an origin with a host wildcard
// ("https://*.example.com"), which matches one or more labels
// prepended to the given domain, at any depth, and does not match
// the domain itself.
//
// Origins may include values outside the RP ID's domain, such as
// origins authorized through Related Origin Requests (the
// /.well-known/webauthn document, which the application is
// responsible for serving).
Origins []string
// RequireUserVerification causes Login to fail with
// [ErrUserVerificationRequired] unless the authenticator performed
// user verification (e.g. PIN or biometrics). NewLogin and
// NewLoginForUser then request userVerification: "required", so
// clients prompt for verification rather than return an assertion
// that would be rejected. It also causes NewRegistration to request
// userVerification: "required", so that clients fail credential
// creation on authenticators that can't satisfy it, rather than
// registering a credential that can never log in. Register itself
// does not check the user verification flag, so that conditional
// (automatic) passkey creation keeps working.
//
// If false (the default), user verification is requested but not
// required, and its result can be checked with
// [ResponseUserVerified].
RequireUserVerification bool
// Timeout is how long a request returned by NewLogin or
// NewLoginForUser remains valid: Login rejects responses to
// expired requests. It is also communicated to the client as a
// ceremony timeout hint (for both registration and login), and is
// the recommended lifetime for requests stored by the application.
//
// If zero, it defaults to five minutes. Conditional UI (autofill)
// logins may warrant a longer timeout, as the prompt can sit idle
// for a while before the user engages with it.
Timeout time.Duration
}
Options configures optional RelyingParty behaviors. The zero value is a sensible default.
type RelyingParty ¶
type RelyingParty struct {
}
RelyingParty verifies registrations and logins for a fixed RP ID.
A relying party is defined by two values with different lifetimes. The RP ID is its identity: it scopes which credentials exist for it, it is stored in every credential, and it can never change (there is no support for RP ID rotation, nor for the appid extension, which would allow asserting credentials registered through the legacy U2F API under their old AppID URL). Options.Origins is a deployment detail: it gates where ceremonies may be performed, and can change freely, for example when sign-in moves to a different subdomain. Generally, the registrable domain of the site (e.g. example.com) is a good RP ID.
func NewRelyingParty ¶
func NewRelyingParty(rpID string, opts *Options) (*RelyingParty, error)
NewRelyingParty returns a RelyingParty for the given RP ID.
By default, the accepted origins are "https://" followed by the RP ID and all its "https://" subdomains, with no port; set Options.Origins to restrict or extend them.
func (*RelyingParty) Login ¶
func (rp *RelyingParty) Login(request, responseJSON []byte, passkeys []string) (matched int, err error)
Login verifies a login response against the request returned by RelyingParty.NewLogin and the given user's passkey records.
responseJSON is the JSON serialization of the PublicKeyCredential returned by navigator.credentials.get() (as produced by its toJSON() method).
On success, it returns the index in passkeys of the record the response was verified against.
Login verifies that the signature is valid, with the matched record's public key, over the authenticator data and the hash of the client data; that the client data is well-formed JSON with type "webauthn.get", the request's challenge, an accepted origin, and crossOrigin absent or false; that the authenticator data carries the hash of the RP ID and has the user presence flag set (and the user verification flag, if Options.RequireUserVerification is set); that the request has not expired; and that the asserted user handle, if present, matches the request for user-scoped ceremonies.
The signature counter is not checked (it is zero for the major passkey providers), and the response's backup and user verification flags can be inspected with ResponseBackedUp and ResponseUserVerified.
Most failures are reported as generic errors, with details for logging: the correct application behavior is the same for all of them. Three failures that call for distinct handling are reported as sentinel errors, documented below; they explain why a login failed, for the application's benefit, and none of them mean the login can be treated as successful.
If the response is valid but its credential ID matches none of passkeys, Login returns ErrUnknownCredential: the user asserted a credential the server no longer (or never) had a record of, for example one deleted from account settings but still present in the user's passkey provider.
If the response is valid but user verification was not performed while Options.RequireUserVerification is set, Login returns ErrUserVerificationRequired, for example when the requirement was enabled after a credential incapable of user verification was registered.
If the response is valid but the request is older than Options.Timeout, Login returns ErrRequestExpired: the application can transparently retry with a fresh request, which is routine for conditional UI (autofill) prompts left idle.
func (*RelyingParty) NewLogin ¶
func (rp *RelyingParty) NewLogin() (request, optionsJSON []byte, err error)
NewLogin begins a login ceremony.
optionsJSON is a PublicKeyCredentialRequestOptions object to be passed to navigator.credentials.get() as the publicKey field, with an empty allowCredentials list. It can be used both for modal and for conditional UI (autofill) flows.
request is an opaque value to be stored by the application (see Challenges, and RequestID for a storage key) and passed to Login.
Note that a login ceremony is not started for a specific user: the user is identified by the response, using Inspect.
func (*RelyingParty) NewLoginForUser ¶
func (rp *RelyingParty) NewLoginForUser(userID string, passkeys []string) (request, optionsJSON []byte, err error)
NewLoginForUser begins a login ceremony for a known user, such as a re-authentication prompt before a sensitive operation.
The user's passkey records are communicated to the client as allowCredentials, so the client offers only that user's credentials instead of an account picker. The response is verified with RelyingParty.Login, passing the same records; Login fails if the response asserts a user handle other than userID.
Responses to user-scoped ceremonies may omit the user handle, in which case Inspect returns an empty user ID; in this flow the application already knows the user.
func (*RelyingParty) NewRegistration ¶
func (rp *RelyingParty) NewRegistration(user User, passkeys []string) (optionsJSON []byte, err error)
NewRegistration begins the registration of a new passkey for user.
passkeys is the user's full list of existing passkey records, or nil if the user has none. They are communicated to the client as excludeCredentials, so an authenticator that already holds one of them for this RP will refuse to create a new credential, and the user won't end up with duplicate passkeys on the same authenticator.
optionsJSON is a PublicKeyCredentialCreationOptions object to be passed to navigator.credentials.create() as the publicKey field, requesting a discoverable credential (residentKey: "required") with attestation "none" and the credProps extension (which reports back whether the created credential is actually discoverable).
Registration is stateless for the server: there is no request value to store, and the response is verified by RelyingParty.Register.
func (*RelyingParty) Register ¶
func (rp *RelyingParty) Register(responseJSON []byte) (passkey string, err error)
Register verifies a registration response and, on success, returns the new passkey record, to be stored for the signed-in user whose session initiated the registration.
responseJSON is the JSON serialization of the PublicKeyCredential returned by navigator.credentials.create() (as produced by its toJSON() method).
Register fails if the response reports (via the credProps extension) that the created credential is not discoverable. Absence of the extension output is accepted: some clients (notably Safari) never report it, and enforcement of discoverability rests on the client's residentKey: "required" obligation.
Register does not require the user presence or user verification flags, so that conditional (automatic) passkey creation flows, in which the user does not interact with a prompt, are supported. User verification is enforced, per Options.RequireUserVerification, at login.
Register verifies that the client data is well-formed JSON with type "webauthn.create" and an accepted origin, and that the authenticator data carries the hash of the RP ID. The challenge is not verified; see the Challenges section of the package documentation.
type User ¶
type User struct {
// ID is the opaque user ID; see [User IDs].
//
// [User IDs]: #hdr-User_IDs
ID string
// Name is a human-readable identifier for the account, such as a
// username or email address. It is displayed in credential pickers
// and stored by the authenticator, but never returned to the server
// or used in the protocol. It is also sent as the WebAuthn
// displayName, which credential providers ignore in practice.
Name string
}
User identifies the account a passkey is being registered for.