// 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$ // // 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.) package passkey // 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 ErrRequestExpired 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 ErrUnknownCredential 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. var ErrUserVerificationRequired error // 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 AAGUID(passkey string) ([16]byte, error) // 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 BackedUp(passkey string) (bool, error) // 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 CredentialID(passkey string) ([]byte, error) // 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 Inspect(responseJSON []byte) (requestID, userID string, err error) // 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 RequestID(request []byte) string // 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 ResponseBackedUp(responseJSON []byte) (bool, error) // 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 ResponseUserVerified(responseJSON []byte) (bool, error) // 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. func Transports(passkey string) ([]string, error) // Options configures optional [RelyingParty] behaviors. // The zero value is a sensible default. 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 } // 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. type RelyingParty struct { } // 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 NewRelyingParty(rpID string, opts *Options) (*RelyingParty, 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 (rp *RelyingParty) Login(request, responseJSON []byte, passkeys []string) (matched int, 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]. // // [Challenges]: #hdr-Challenges func (rp *RelyingParty) NewLogin() (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 (rp *RelyingParty) NewLoginForUser(userID string, passkeys []string) (request, 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 (rp *RelyingParty) NewRegistration(user User, passkeys []string) (optionsJSON []byte, 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. func (rp *RelyingParty) Register(responseJSON []byte) (passkey string, err error) // User identifies the account a passkey is being registered for. 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 }