From cd77d5f21f4383d60c2e324bc1d1b1541dfb840d Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 13 Aug 2024 22:37:04 -0700 Subject: [PATCH 01/18] Begin drafting back end requirements --- content/docs/implement/backend.md | 38 +++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 content/docs/implement/backend.md diff --git a/content/docs/implement/backend.md b/content/docs/implement/backend.md new file mode 100644 index 00000000..1556e18b --- /dev/null +++ b/content/docs/implement/backend.md @@ -0,0 +1,38 @@ +--- +title: "Back End Requirements" +description: "Guidance on server-side handling of passkeys registration and authentication" +lead: "" +date: 2024-08-13T12:00:00.000Z +draft: false +images: [] +menu: + docs: + parent: "implement" +weight: 501 +toc: true +--- + +## Back End Requirements + +The back end drives WebAuthn ceremonies through four primary responsibilities: + +1. Generate registration options +2. Verify registration responses +3. Generate authentication options +4. Verify authentication responses + +A Relying Party has responsibilities along the way that they must be mindful of as they take on +these responsibilities. The guidance below is intended to identify best practices to securely +incorporating passkeys-based authentication into your website. + +**Please note that this guidance is not applicable to any specific server implementation.** +It is intended to be a launching point; care should be taken as you consider how best to adapt this +guidance for your particular site. + +## 1. Generate registration options + +## 2. Verify registration responses + +## 3. Generate authentication options + +## 4. Verify authentication responses From d3fd88976906bf6a43d778a21838a0621375111b Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 13 Aug 2024 22:37:20 -0700 Subject: [PATCH 02/18] Show Implementation in the sidebar --- content/docs/implement/_index.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 content/docs/implement/_index.md diff --git a/content/docs/implement/_index.md b/content/docs/implement/_index.md new file mode 100644 index 00000000..481c8e36 --- /dev/null +++ b/content/docs/implement/_index.md @@ -0,0 +1,11 @@ +--- +title : "Implementation" +description: "Adding passkeys to your website" +lead: "" +date: 2022-09-24T15:57:34.857Z +draft: false +images: [] +weight: 500 +sidebar: + collapsed: true +--- From d92f1f5807d6250db98a5dca1f0f329e18757563 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 13 Aug 2024 22:39:09 -0700 Subject: [PATCH 03/18] Remove redundant title --- content/docs/implement/backend.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/content/docs/implement/backend.md b/content/docs/implement/backend.md index 1556e18b..77c904c9 100644 --- a/content/docs/implement/backend.md +++ b/content/docs/implement/backend.md @@ -12,8 +12,6 @@ weight: 501 toc: true --- -## Back End Requirements - The back end drives WebAuthn ceremonies through four primary responsibilities: 1. Generate registration options From f0eb7d041c4b2c819345b3d17b96b10300736b5f Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Tue, 13 Aug 2024 22:40:13 -0700 Subject: [PATCH 04/18] Workshop intro --- content/docs/implement/backend.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/content/docs/implement/backend.md b/content/docs/implement/backend.md index 77c904c9..447c221e 100644 --- a/content/docs/implement/backend.md +++ b/content/docs/implement/backend.md @@ -19,9 +19,8 @@ The back end drives WebAuthn ceremonies through four primary responsibilities: 3. Generate authentication options 4. Verify authentication responses -A Relying Party has responsibilities along the way that they must be mindful of as they take on -these responsibilities. The guidance below is intended to identify best practices to securely -incorporating passkeys-based authentication into your website. +The guidance below is intended to identify best practices to fulfill these responsibilities and +securely incorporate passkeys-based authentication into your website. **Please note that this guidance is not applicable to any specific server implementation.** It is intended to be a launching point; care should be taken as you consider how best to adapt this From d259b28c1655b8e4d2108cfac6bca51297dc2d74 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 09:37:38 -0800 Subject: [PATCH 05/18] Refactor for Hinode refactor --- content/docs/implement/_index.md | 11 ----------- .../implement => en/docs/implementation}/backend.md | 8 -------- data/docs.yml | 5 +++++ 3 files changed, 5 insertions(+), 19 deletions(-) delete mode 100644 content/docs/implement/_index.md rename content/{docs/implement => en/docs/implementation}/backend.md (90%) diff --git a/content/docs/implement/_index.md b/content/docs/implement/_index.md deleted file mode 100644 index 481c8e36..00000000 --- a/content/docs/implement/_index.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -title : "Implementation" -description: "Adding passkeys to your website" -lead: "" -date: 2022-09-24T15:57:34.857Z -draft: false -images: [] -weight: 500 -sidebar: - collapsed: true ---- diff --git a/content/docs/implement/backend.md b/content/en/docs/implementation/backend.md similarity index 90% rename from content/docs/implement/backend.md rename to content/en/docs/implementation/backend.md index 447c221e..911aa682 100644 --- a/content/docs/implement/backend.md +++ b/content/en/docs/implementation/backend.md @@ -1,15 +1,7 @@ --- title: "Back End Requirements" description: "Guidance on server-side handling of passkeys registration and authentication" -lead: "" date: 2024-08-13T12:00:00.000Z -draft: false -images: [] -menu: - docs: - parent: "implement" -weight: 501 -toc: true --- The back end drives WebAuthn ceremonies through four primary responsibilities: diff --git a/data/docs.yml b/data/docs.yml index 42703431..f5d8e240 100644 --- a/data/docs.yml +++ b/data/docs.yml @@ -10,6 +10,11 @@ - title: Reauthentication link: reauth +- title: Implementation + pages: + - title: Backend Requirements + link: backend + - title: Advanced pages: - title: Related Origin Requests From 06eee0dd3c4a09cc849c39b9f873bf11d0d9f56e Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 10:58:49 -0800 Subject: [PATCH 06/18] Write some words for reg options generation --- content/en/docs/implementation/backend.md | 135 ++++++++++++++++++++-- 1 file changed, 128 insertions(+), 7 deletions(-) diff --git a/content/en/docs/implementation/backend.md b/content/en/docs/implementation/backend.md index 911aa682..d415a01c 100644 --- a/content/en/docs/implementation/backend.md +++ b/content/en/docs/implementation/backend.md @@ -1,27 +1,148 @@ --- -title: "Back End Requirements" +title: "Backend Requirements" description: "Guidance on server-side handling of passkeys registration and authentication" date: 2024-08-13T12:00:00.000Z --- -The back end drives WebAuthn ceremonies through four primary responsibilities: +The backend drives WebAuthn ceremonies through four primary responsibilities: 1. Generate registration options 2. Verify registration responses 3. Generate authentication options 4. Verify authentication responses -The guidance below is intended to identify best practices to fulfill these responsibilities and -securely incorporate passkeys-based authentication into your website. +The guidance below identifies best practices to fulfill these responsibilities and securely incorporate passkeys-based authentication into your website. + +{{< alert type="info" >}} +**Please note that this is general guidance; it does not account for any one specific server implementation.** +It is intended to be a launching point. Care should be taken as you consider how best to adapt this guidance for your particular site. +{{< /alert >}} + +## Data Structures + +A site that uses passkeys should be prepared to store the following "`PasskeyModel`" database model: + +```ts +type PasskeyModel = { + // SQL: Store as `TEXT`. Index this column + id: Base64URLString; + // SQL: Store raw bytes as `BYTEA`/`BLOB`/etc... + publicKey: Uint8Array; + // SQL: Foreign Key to an instance of your internal user model + user: UserModel; + // SQL: Store as `TEXT`. Index this column. A UNIQUE constraint on + // (webAuthnUserID + user) also achieves maximum user privacy + webauthnUserID: Base64URLString; + // SQL: `INT` or whatever similar type is supported + counter: number; + // SQL: `BOOL` or whatever similar type is supported + backupEligible: boolean; + // SQL: `BOOL` or whatever similar type is supported + backupStatus: boolean; + // SQL: `VARCHAR(255)` and store string array as a CSV string + // Ex: ble,cable,hybrid,internal,nfc,smart-card,usb + transports?: AuthenticatorTransport[]; +}; +``` + +The association between an instance of this `PasskeyModel` and an instance of +your site's "`UserModel`" should be established in a way that makes sense +for your site's database architecture. +For the purposes of the documentation below, the following `UserModel` is assumed: + +```ts +type UserModel = { + // No assumption is made of how users are assigned IDs within the database + id: any; + // An email address, username, or other identifiable information + username: string; +}; +``` -**Please note that this guidance is not applicable to any specific server implementation.** -It is intended to be a launching point; care should be taken as you consider how best to adapt this -guidance for your particular site. ## 1. Generate registration options +Registration options define several values that authenticators need to help associate a passkey with your site. Some of the most important values include the following: + +- **RP ID:** A scoped domain name of the site on which the passkey should be available. The browser will require this to be part of the effective domain of the site hosting the registration ceremony. +- **User ID:** A unique, random identifier for the user account that the passkey will be registered to. + This **should not be personally-identifying information** like an email address/username/`UserModel.username`/etc...! + +Some of the options that get passed in to WebAuthn's `navigator.credentials.create()` call need to be of type `Uint8Array`. Unfortunately byte arrays do not serialize well into a JSON representation, making it tricky to get these values from the backend to the frontend. + +It is suggested, then, that the "`generateRegistrationOptions()`" method returns a value of type [`PublicKeyCredentialCreationOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialcreationoptionsjson) that uses **Base64URL** encoding to encode such byte arrays to a value that is easier to transmit to the frontend as JSON: + +```ts +/** + * Generate passkey registration options + */ +async function generateRegistrationOptions( + currentUser: UserModel, +): PublicKeyCredentialCreationOptionsJSON { + // A domain name for your site (e.g. "passkeys.dev") + const rpID: string = process.env.RP_ID; + // A human-readable name for your website (e.g. "Passkeys Developer Resources") + const rpName: string = process.env.RP_NAME; + + // Generate one-time-use random bytes for the authenticator to sign + const challenge: Uint8Array = await pseudocodeGenerateChallenge(currentUser); + // Generate or retrieve a pseudonymous, WebAuthn-specific user identifier as random bytes + const userID: Uint8Array = await pseudocodeGetWebAuthnUserID(currentUser); + // Get a list of the user's currently registered passkeys to prevent re-registration + const userCurrentPasskeys: PasskeyModel[] = await pseudocodeGetCurrentPasskeys(currentUser); + + return { + rp: { + id: rpID, + name: rpName, + }, + user: { + id: pseudocodeBytesToBase64URLString(userID), + name: currentUser.username, + displayName: '', + }, + challenge: pseudocodeBytesToBase64URLString(challenge), + pubKeyCredParams: [ + { alg: -8, type: 'public-key' }, + { alg: -7, type: 'public-key' }, + { alg: -257, type: 'public-key' }, + ], + attestation: 'direct', + excludeCredentials: userCurrentPasskeys.map((passkey) => ({ + id: passkey.id, + transports: passkey.transports, + type: 'public-key', + })), + authenticatorSelection: { + residentKey: 'preferred', + userVerification: 'preferred', + }, + }; +} +``` + +These options should then be remembered since some values will be needed to verify the registration +response generated for these options: + +```ts +const regOptions = await generateRegistrationOptions(currentUser); + +await pseudocodeSaveCurrentRegistrationOptions(currentUser, regOptions); +``` + +`regOptions` can then be transmitted to the frontend (docs "coming soon") as JSON for the frontend +to eventually pass them in to `navigator.credentials.create()`. + ## 2. Verify registration responses ## 3. Generate authentication options ## 4. Verify authentication responses + +## Third-Party Libraries + +Many third-party libraries exist to simplify the job of generating options and verifying responses. +These libraries can help reduce your maintenance burden of supporting passkeys by virtue of +their ability to keep up with WebAuthn API changes. Head over to +{{< link "../tools-libraries/libraries" >}}Tools & Libraries > Libraries{{< /link >}} +for a list of maintained libraries in your backend's language. From 665eabf69797a064b1f7420961776da6fe887659 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 10:58:54 -0800 Subject: [PATCH 07/18] Make code highlighting prettier --- config/_default/hugo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index b5bea7d4..11652da5 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -10,3 +10,7 @@ enableRobotsTXT = true [[module.imports]] path = 'github.com/gethinode/hinode' + +[markup] + [markup.highlight] + style = 'onedark' From 679bbdecf7e536b99321ce502839f5ae8c056284 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 11:13:59 -0800 Subject: [PATCH 08/18] Tweak "coming soon" --- content/en/docs/implementation/backend.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/en/docs/implementation/backend.md b/content/en/docs/implementation/backend.md index d415a01c..9ffb696d 100644 --- a/content/en/docs/implementation/backend.md +++ b/content/en/docs/implementation/backend.md @@ -130,7 +130,7 @@ const regOptions = await generateRegistrationOptions(currentUser); await pseudocodeSaveCurrentRegistrationOptions(currentUser, regOptions); ``` -`regOptions` can then be transmitted to the frontend (docs "coming soon") as JSON for the frontend +`regOptions` can then be transmitted to the frontend (docs coming soon) as JSON for the frontend to eventually pass them in to `navigator.credentials.create()`. ## 2. Verify registration responses From ea74680ef6ec9614d99a673ba75da4e713783ba0 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 11:54:12 -0800 Subject: [PATCH 09/18] Add a frontend reqs doc to link to --- content/en/docs/implementation/frontend.md | 7 +++++++ data/docs.yml | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 content/en/docs/implementation/frontend.md diff --git a/content/en/docs/implementation/frontend.md b/content/en/docs/implementation/frontend.md new file mode 100644 index 00000000..6dda43f5 --- /dev/null +++ b/content/en/docs/implementation/frontend.md @@ -0,0 +1,7 @@ +--- +title: "Frontend Requirements" +description: "Guidance on handling passkeys registration and authentication in the browser" +date: 2025-01-02T12:00:00.000Z +--- + +Coming Soon... diff --git a/data/docs.yml b/data/docs.yml index f5d8e240..d4cbe079 100644 --- a/data/docs.yml +++ b/data/docs.yml @@ -14,6 +14,8 @@ pages: - title: Backend Requirements link: backend + - title: Frontend Requirements + link: frontend - title: Advanced pages: From 88b96f02a0c8cdac38147dd696b4f2133622cd26 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 13:39:51 -0800 Subject: [PATCH 10/18] Make URLs less jumpy on hover --- assets/scss/theme/theme.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scss/theme/theme.scss b/assets/scss/theme/theme.scss index 020cbb46..4edda939 100644 --- a/assets/scss/theme/theme.scss +++ b/assets/scss/theme/theme.scss @@ -3,7 +3,7 @@ } a:hover { - font-weight: bold; + text-decoration: underline; } .fade-out-text { From cc78187459201237f19a376da50c92f141e23729 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 14:17:15 -0800 Subject: [PATCH 11/18] Define reg response verification --- content/en/docs/implementation/backend.md | 96 ++++++++++++++++++++++- 1 file changed, 93 insertions(+), 3 deletions(-) diff --git a/content/en/docs/implementation/backend.md b/content/en/docs/implementation/backend.md index 9ffb696d..986222bf 100644 --- a/content/en/docs/implementation/backend.md +++ b/content/en/docs/implementation/backend.md @@ -125,16 +125,106 @@ These options should then be remembered since some values will be needed to veri response generated for these options: ```ts +// The ID of the user's auth session that was created after the user logged in +const sessionID = request.session.id; + +// User data associated with the current auth session +const currentUser: UserModel = await getUserData(sessionID); + const regOptions = await generateRegistrationOptions(currentUser); -await pseudocodeSaveCurrentRegistrationOptions(currentUser, regOptions); +// Persist the options so we can reference values in them during verification +await pseudocodeSaveRegistrationOptions(sessionID, regOptions); ``` -`regOptions` can then be transmitted to the frontend (docs coming soon) as JSON for the frontend -to eventually pass them in to `navigator.credentials.create()`. +`regOptions` can then be transmitted to the frontend as JSON for +{{< link "./frontend.md" >}}the frontend to eventually pass in to +`navigator.credentials.create()`.{{< /link >}} ## 2. Verify registration responses +Once the frontend has taken the registration options above and fed them into WebAuthn, +and after the user succeeds in creating a passkey with their chosen passkey provider, +then the subsequent registration response returned from the WebAuthn call +will need to be sent to the backend for verification. + +As JSON is a popular way to send the response back, +the backend method "`verifyRegistrationResponse()`" should prepare to accept a value +in the shape of [`RegistrationResponseJSON`](https://w3c.github.io/webauthn/#dictdef-registrationresponsejson). + +```ts +/** + * Check that the WebAuthn registration data represents a well-formed passkey + */ +async function verifyRegistrationResponse( + currentUser: UserModel, + registrationOptions: PublicKeyCredentialCreationOptionsJSON, + registrationResponse: RegistrationResponseJSON, +): VerifiedRegistration { + try { + // TODO: Write basic attestation-less response verification here? + } catch (error) { + throw new Error(`Couldn't verify registration response`, { cause: error }); + } + + return { + passkey: { + id: registrationResponse.id, + publicKey: new Uint8Array([...]), + counter: 0, + backupEligible: true, + backupStatus: true, + transports: ['internal', 'hybrid'], + }, + } +} + +type VerifiedRegistration = { + passkey: { + id: Base64URLString; + publicKey: Uint8Array; + counter: number; + backupEligible: boolean; + backupStatus: boolean; + transports?: AuthenticatorTransport[]; + } +}; +``` + +```ts +// The ID of the user's auth session that was created after the user logged in +const sessionID = request.session.id; + +// User data associated with the current auth session +const currentUser: UserModel = await getUserData(sessionID); + +// Retrieve registration options for the current attempt to check for expected values +const regOptions: PublicKeyCredentialCreationOptionsJSON = + await pseudocodeRetrieveAndDeleteRegistrationOptions(sessionID); + +let passkey; +try { + const verification = await verifyRegistrationResponse(currentUser, regOptions, regResponse); + passkey = verification.passkey; + // User successfully registered a passkey, continue +} catch (err) { + console.error(err); + // Something went wrong, notify the user accordingly +} +``` + +Assuming successful creation, information about the newly-created passkey +should then get stored as a `PasskeyModel` record in the database: + +```ts +/** + * - Use `currentUser` for the foreign key needed for `PasskeyModel.user` + * - Use `regOptions.user.id` for `PasskeyModel.webauthnUserID` + * - Use the values in `passkey` to populate the remaining `PasskeyModel` fields + */ +await pseudocodeSaveNewPasskey(currentUser, regOptions, passkey); +``` + ## 3. Generate authentication options ## 4. Verify authentication responses From 7b3a32e4635715cc2c92462bb50696cf31c21c10 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Thu, 2 Jan 2025 15:04:42 -0800 Subject: [PATCH 12/18] Add auth opts generation and resp verification --- content/en/docs/implementation/backend.md | 188 +++++++++++++++++++++- 1 file changed, 180 insertions(+), 8 deletions(-) diff --git a/content/en/docs/implementation/backend.md b/content/en/docs/implementation/backend.md index 986222bf..89e009fc 100644 --- a/content/en/docs/implementation/backend.md +++ b/content/en/docs/implementation/backend.md @@ -81,13 +81,16 @@ async function generateRegistrationOptions( ): PublicKeyCredentialCreationOptionsJSON { // A domain name for your site (e.g. "passkeys.dev") const rpID: string = process.env.RP_ID; - // A human-readable name for your website (e.g. "Passkeys Developer Resources") + + // A human-read able name for your website (e.g. "Passkeys Developer Resources") const rpName: string = process.env.RP_NAME; // Generate one-time-use random bytes for the authenticator to sign const challenge: Uint8Array = await pseudocodeGenerateChallenge(currentUser); + // Generate or retrieve a pseudonymous, WebAuthn-specific user identifier as random bytes const userID: Uint8Array = await pseudocodeGetWebAuthnUserID(currentUser); + // Get a list of the user's currently registered passkeys to prevent re-registration const userCurrentPasskeys: PasskeyModel[] = await pseudocodeGetCurrentPasskeys(currentUser); @@ -157,14 +160,13 @@ in the shape of [`RegistrationResponseJSON`](https://w3c.github.io/webauthn/#dic * Check that the WebAuthn registration data represents a well-formed passkey */ async function verifyRegistrationResponse( - currentUser: UserModel, registrationOptions: PublicKeyCredentialCreationOptionsJSON, registrationResponse: RegistrationResponseJSON, ): VerifiedRegistration { try { // TODO: Write basic attestation-less response verification here? - } catch (error) { - throw new Error(`Couldn't verify registration response`, { cause: error }); + } catch (err) { + throw new Error(`Couldn't verify registration response`, { cause: err }); } return { @@ -195,16 +197,16 @@ type VerifiedRegistration = { // The ID of the user's auth session that was created after the user logged in const sessionID = request.session.id; -// User data associated with the current auth session -const currentUser: UserModel = await getUserData(sessionID); - // Retrieve registration options for the current attempt to check for expected values const regOptions: PublicKeyCredentialCreationOptionsJSON = await pseudocodeRetrieveAndDeleteRegistrationOptions(sessionID); +// Assume `RegistrationResponseJSON` was sent back as JSON via a POST command +const regResponse = request.body; + let passkey; try { - const verification = await verifyRegistrationResponse(currentUser, regOptions, regResponse); + const verification = await verifyRegistrationResponse(regOptions, regResponse); passkey = verification.passkey; // User successfully registered a passkey, continue } catch (err) { @@ -217,6 +219,9 @@ Assuming successful creation, information about the newly-created passkey should then get stored as a `PasskeyModel` record in the database: ```ts +// User data associated with the current auth session +const currentUser: UserModel = await getUserData(sessionID); + /** * - Use `currentUser` for the foreign key needed for `PasskeyModel.user` * - Use `regOptions.user.id` for `PasskeyModel.webauthnUserID` @@ -225,10 +230,177 @@ should then get stored as a `PasskeyModel` record in the database: await pseudocodeSaveNewPasskey(currentUser, regOptions, passkey); ``` +The user is now ready to use their passkey for subsequent authentication. + ## 3. Generate authentication options +For similar reasons, authentication options generation should aim to return a value +shaped like [`PublicKeyCredentialRequestOptionsJSON`](https://w3c.github.io/webauthn/#dictdef-publickeycredentialrequestoptionsjson). +This will simplify the work of sending these options to the front end as JSON: + +```ts +/** + * Create authentication options for a user to sign in with a passkey + */ +async function generateAuthenticationOptions( + currentUser?: UserModel, +): PublicKeyCredentialRequestOptionsJSON { + // This must be the same value specified during registration (e.g. "passkeys.dev") + const rpID: string = process.env.RP_ID; + + // Generate one-time-use random bytes for the authenticator to sign + const challenge: Uint8Array = await pseudocodeGenerateChallenge(currentUser); + + // If you know the user that's trying to log in then get the list of passkeys they can use + // Hint: You won't know the user if you're using conditional UI... + let userCurrentPasskeys: PasskeyModel[] = []; + if (currentUser) { + userCurrentPasskeys = await pseudocodeGetCurrentPasskeys(currentUser); + } + + return { + rpId: rpID, + challenge: pseudocodeBytesToBase64URLString(challenge), + allowCredentials: userCurrentPasskeys.map((passkey) => ({ + id: passkey.id, + transports: passkey.transports, + type: 'public-key', + })), + userVerification: 'preferred', + } +} +``` + +Generate options, then store them for *someone* to use to log in with a registered passkey: + +```ts +// The ID of an UNAUTHENTICATED user session, established when a user hits the login page +const unknownUserSessionID = request.session.id; + +// User data associated with the provided account identifier (email, username, etc...) +let currentUser: UserModel | undefined = undefined; +if (enteredAccountID) { + currentUser = await getUserData(enteredAccountID); +} + +const authOptions = await generateAuthenticationOptions(currentUser); + +// Persist the options so we can reference values in them during verification +await pseudocodeSaveAuthenticationOptions(unknownUserSessionID, authOptions); +``` + +Send these options to the {{< link "./frontend.md" >}}frontend{{< /link >}} to have the user attempt to log in. + +{{< alert type="info" >}} +WebAuthn is capable of handling both "passwordless" and "usernameless" authentication flows: + +- **Passwordless** authentication often starts with the user typing in an account identifier. + In this flow the Relying Party should populate `allowCredentials` with that user's + registered passkeys to lean on the browser to help the user understand when they have a passkey + for the site. + +- **Usernameless** authentication involves initiating a WebAuthn authentication attempt + without any upfront knowledge about the user's identity. + Instead, the user first chooses to use a registered passkey in response to a WebAuthn call + with an empty `allowCredentials.` + Next the website checks that it recognizes the passkey ID, + verifies the signature in the response with the public key for that passkey, + then logs the user in as whatever user is assigned to `PasskeyModel.user` + when the passkey was created. +{{< /alert >}} + ## 4. Verify authentication responses +Assuming successful use of WebAuthn to generate an authentication response on the frontend, +the Relying Party should now verify the instance of +[`AuthenticationResponseJSON`](https://w3c.github.io/webauthn/#dictdef-authenticationresponsejson): + +```ts +/** + * Make sure the authentication response is valid for the specified, registered passkey + */ +async function verifyAuthenticationResponse( + authOptions: PublicKeyCredentialRequestOptionsJSON, + authResponse: AuthenticationResponseJSON, + registeredPasskey: PasskeyModel, +): VerifiedAuthentication { + try { + // TODO: Write basic authentication verification here? + } catch (err) { + throw new Error(`Couldn't verify authentication response`, { cause: err }); + } + + return { + passkey: { + id: registeredPasskey.id, + newCounter: 0, + backupEligible: true, + backupStatus: true, + }, + } +} + +type VerifiedAuthentication = { + passkey: { + id: Base64URLString; + newCounter: number; + backupEligible: boolean; + backupStatus: boolean; + } +}; +``` + +Call the method, then log the user in upon successful verification: + +```ts +// The ID of an UNAUTHENTICATED user session, established when a user hits the login page +const unknownUserSessionID = request.session.id; + +// Retrieve registration options for the current attempt to check for expected values +const authOptions: PublicKeyCredentialRequestOptionsJSON = + await pseudocodeRetrieveAndDeleteAuthenticationOptions(unknownUserSessionID); + +// Assume `AuthenticationResponseJSON` was sent back as JSON via a POST command +const authResponse = request.body; + +// Make sure the credential ID specified in the response is one the site recognizes +let registeredPasskey: PasskeyModel; +try { + registeredPasskey = await pseudocodeGetRegisteredPasskey(authResponse.id); +} catch (err) { + console.error(err); + // Something went wrong, notify the user accordingly + throw new Error('Unrecognized passkey ID'); +} + +// Now try to verify the response +let passkey; +try { + const verification = await verifyAuthenticationResponse( + authOptions, + authResponse, + registeredPasskey, + ); + passkey = verification.passkey; + // User successfully registered a passkey, continue +} catch (err) { + console.error(err); + // Something went wrong, notify the user accordingly +} +``` + +Assuming successful verification, log the user in and update information about the passkey: + +```ts +// "Log the user in", whatever that looks like for your site +request.session.user = await pseudocodeGetUserForPasskeyID(passkey.id); + +// Update `PasskeyModel.counter` and `PasskeyModel.backupStatus` in the database +await pseudocodeUpdatePasskeyRecord(passkey); +``` + +The user has now successfully used a passkey to log in. + ## Third-Party Libraries Many third-party libraries exist to simplify the job of generating options and verifying responses. From 689cb5c422cfc50076c97d357bfe4e7d833b0110 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 3 Jan 2025 15:31:25 -0800 Subject: [PATCH 13/18] Revert text underline (for #413) --- assets/scss/theme/theme.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/scss/theme/theme.scss b/assets/scss/theme/theme.scss index 4edda939..020cbb46 100644 --- a/assets/scss/theme/theme.scss +++ b/assets/scss/theme/theme.scss @@ -3,7 +3,7 @@ } a:hover { - text-decoration: underline; + font-weight: bold; } .fade-out-text { From b382cc3edb1d7722f9c8ea665066e0f7b1a97703 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 24 Oct 2025 10:28:48 -0700 Subject: [PATCH 14/18] Stop Advanced from being clickable --- content/en/docs/advanced/_index.md | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 content/en/docs/advanced/_index.md diff --git a/content/en/docs/advanced/_index.md b/content/en/docs/advanced/_index.md deleted file mode 100644 index 52e2cca1..00000000 --- a/content/en/docs/advanced/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Advanced -description: "Advanced developer guides." ---- \ No newline at end of file From 6149e6586331f9646ad2f1e04eeb3eb266613657 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 24 Oct 2025 10:29:02 -0700 Subject: [PATCH 15/18] Address backend TODOs with pseudocode --- content/en/docs/implementation/backend.md | 55 ++++++++++++++--------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/content/en/docs/implementation/backend.md b/content/en/docs/implementation/backend.md index 89e009fc..d0dcc912 100644 --- a/content/en/docs/implementation/backend.md +++ b/content/en/docs/implementation/backend.md @@ -4,18 +4,21 @@ description: "Guidance on server-side handling of passkeys registration and auth date: 2024-08-13T12:00:00.000Z --- -The backend drives WebAuthn ceremonies through four primary responsibilities: +The website's backend plays the following roles in facilitating use of WebAuthn: -1. Generate registration options -2. Verify registration responses -3. Generate authentication options -4. Verify authentication responses +1. Generate registration options for the frontend to call `navigator.credentials.create()` +2. Verify the registration response +3. Generate authentication options for `navigator.credentials.get()` +4. Verify the authentication response -The guidance below identifies best practices to fulfill these responsibilities and securely incorporate passkeys-based authentication into your website. +The guidance below identifies best practices to fulfill these responsibilities +and securely incorporate passkeys-based authentication into your website. -{{< alert type="info" >}} -**Please note that this is general guidance; it does not account for any one specific server implementation.** -It is intended to be a launching point. Care should be taken as you consider how best to adapt this guidance for your particular site. +{{< alert type="info" color="warning" icon="fa-solid fa-triangle-exclamation" >}} +**Please note that this is general guidance; it does not account +for any one specific server implementation.** +It is intended to be a launching point. Care should be taken as you consider +how best to adapt this guidance for your particular site. {{< /alert >}} ## Data Structures @@ -59,7 +62,6 @@ type UserModel = { }; ``` - ## 1. Generate registration options Registration options define several values that authenticators need to help associate a passkey with your site. Some of the most important values include the following: @@ -164,7 +166,13 @@ async function verifyRegistrationResponse( registrationResponse: RegistrationResponseJSON, ): VerifiedRegistration { try { - // TODO: Write basic attestation-less response verification here? + // Consider using a library to verify the registration response + const { + publicKey, + counter, + backupEligible, + backupStatus, + } = pseudocodeVerifyRegistrationResponse(registrationOptions, registrationResponse); } catch (err) { throw new Error(`Couldn't verify registration response`, { cause: err }); } @@ -172,11 +180,11 @@ async function verifyRegistrationResponse( return { passkey: { id: registrationResponse.id, - publicKey: new Uint8Array([...]), - counter: 0, - backupEligible: true, - backupStatus: true, - transports: ['internal', 'hybrid'], + publicKey, + counter, + backupEligible, + backupStatus, + transports: registrationResponse.response.transports, }, } } @@ -291,7 +299,7 @@ await pseudocodeSaveAuthenticationOptions(unknownUserSessionID, authOptions); Send these options to the {{< link "./frontend.md" >}}frontend{{< /link >}} to have the user attempt to log in. -{{< alert type="info" >}} +{{< alert alert-type="info" >}} WebAuthn is capable of handling both "passwordless" and "usernameless" authentication flows: - **Passwordless** authentication often starts with the user typing in an account identifier. @@ -325,7 +333,12 @@ async function verifyAuthenticationResponse( registeredPasskey: PasskeyModel, ): VerifiedAuthentication { try { - // TODO: Write basic authentication verification here? + // Consider using a library to verify the authentication response + const { + counter, + backupEligible, + backupStatus, + } = pseudocodeVerifyAuthenticationResponse(authOptions, authResponse, registeredPasskey); } catch (err) { throw new Error(`Couldn't verify authentication response`, { cause: err }); } @@ -333,9 +346,9 @@ async function verifyAuthenticationResponse( return { passkey: { id: registeredPasskey.id, - newCounter: 0, - backupEligible: true, - backupStatus: true, + newCounter: counter, + backupEligible, + backupStatus, }, } } From 9a875c9bb6260a1d05ed39fbc7b5e61114ebf1d9 Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 24 Oct 2025 10:29:12 -0700 Subject: [PATCH 16/18] Start writing frontend reqs --- content/en/docs/implementation/frontend.md | 33 +++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/content/en/docs/implementation/frontend.md b/content/en/docs/implementation/frontend.md index 6dda43f5..0f9b7d41 100644 --- a/content/en/docs/implementation/frontend.md +++ b/content/en/docs/implementation/frontend.md @@ -4,4 +4,35 @@ description: "Guidance on handling passkeys registration and authentication in t date: 2025-01-02T12:00:00.000Z --- -Coming Soon... +The website's frontend plays the following roles in facilitating use of WebAuthn: + +1. Call `navigator.credentials.create()` +2. Send the registration response to the server +3. Call `navigator.credentials.get()` +4. Send the authentication response to the server + +The guidance below identifies best practices to fulfill these responsibilities +and securely incorporate passkeys-based authentication into your website. + +{{< alert type="info" color="warning" icon="fa-solid fa-triangle-exclamation" >}} +**Please note that this is general guidance; it does not account +for any one specific browser implementation.** +It is intended to be a launching point. Care should be taken as you consider +how best to adapt this guidance for your particular site. +{{< /alert >}} + +## 1. Call `navigator.credentials.create()` + +TODO + +## 2. Send the registration response to the server + +TODO + +## 3. Call `navigator.credentials.get()` + +TODO + +## 4. Send the authentication response to the server + +TODO From e21560d47ce303d9e42548b24fb2738f73c3d84e Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 24 Oct 2025 10:55:45 -0700 Subject: [PATCH 17/18] Don't use list pages for more docs links --- content/en/docs/reference/_index.md | 4 ---- content/en/docs/tools-libraries/_index.md | 4 ---- 2 files changed, 8 deletions(-) delete mode 100644 content/en/docs/reference/_index.md delete mode 100644 content/en/docs/tools-libraries/_index.md diff --git a/content/en/docs/reference/_index.md b/content/en/docs/reference/_index.md deleted file mode 100644 index e65f8b54..00000000 --- a/content/en/docs/reference/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Reference -description: "Detailed references for platforms, specifications, terminology, and other information." ---- \ No newline at end of file diff --git a/content/en/docs/tools-libraries/_index.md b/content/en/docs/tools-libraries/_index.md deleted file mode 100644 index 218291c2..00000000 --- a/content/en/docs/tools-libraries/_index.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -title: Tools & Libraries -description: "Detailed references for platforms, specifications, terminology, and other information." ---- \ No newline at end of file From 587a9ed28afd2b09921a546397c33890d29ad73d Mon Sep 17 00:00:00 2001 From: Matthew Miller Date: Fri, 24 Oct 2025 10:56:17 -0700 Subject: [PATCH 18/18] Add helpers for bytes <-> b64url --- content/en/docs/implementation/frontend.md | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/content/en/docs/implementation/frontend.md b/content/en/docs/implementation/frontend.md index 0f9b7d41..98bd85c6 100644 --- a/content/en/docs/implementation/frontend.md +++ b/content/en/docs/implementation/frontend.md @@ -21,6 +21,82 @@ It is intended to be a launching point. Care should be taken as you consider how best to adapt this guidance for your particular site. {{< /alert >}} +## Using JSON between frontend and backend + +Some of the options that get passed in to WebAuthn's `navigator.credentials.create()` +and `navigator.credentials.get()` must be of type `Uint8Array`. Unfortunately `Uint8Array` values +do not serialize well into a JSON representation, making it tricky to receive and send them +between the backend to the frontend. + +It is suggested that options and responses be shuttled between frontend and backend using +**Base64URL** encoding on `Uint8Array` values. Base64URL `string` representations of these values +are easier to transmit when passed back and forth as JSON. + +The following `base64URLStringToBuffer()` and `bufferToBase64URLString()` methods +can be used to account for the lack of native JavaScript support +in web platforms to encode and decode between `Uint8Array` and Base64URL `string` values: + +### base64URLStringToBuffer() + +```ts +/** + * Convert from a Base64URL-encoded string to an Array Buffer. Best used when converting a + * credential ID from a JSON string to an ArrayBuffer, like in allowCredentials or + * excludeCredentials + * + * Helper method to compliment `bufferToBase64URLString` + */ +export function base64URLStringToBuffer(base64URLString: string): ArrayBuffer { + // Convert from Base64URL to Base64 + const base64 = base64URLString.replace(/-/g, '+').replace(/_/g, '/'); + /** + * Pad with '=' until it's a multiple of four + * (4 - (85 % 4 = 1) = 3) % 4 = 3 padding + * (4 - (86 % 4 = 2) = 2) % 4 = 2 padding + * (4 - (87 % 4 = 3) = 1) % 4 = 1 padding + * (4 - (88 % 4 = 0) = 4) % 4 = 0 padding + */ + const padLength = (4 - (base64.length % 4)) % 4; + const padded = base64.padEnd(base64.length + padLength, '='); + + // Convert to a binary string + const binary = atob(padded); + + // Convert binary string to buffer + const buffer = new ArrayBuffer(binary.length); + const bytes = new Uint8Array(buffer); + + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return buffer; +} +``` + +### bufferToBase64URLString() + +```ts +/** + * Convert the given array buffer into a Base64URL-encoded string. Ideal for converting various + * credential response ArrayBuffers to string for sending back to the server as JSON. + * + * Helper method to compliment `base64URLStringToBuffer` + */ +export function bufferToBase64URLString(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + let str = ''; + + for (const charCode of bytes) { + str += String.fromCharCode(charCode); + } + + const base64String = btoa(str); + + return base64String.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''); +} +``` + ## 1. Call `navigator.credentials.create()` TODO