|
1 | 1 | --- |
2 | | -title: "Back End Requirements" |
| 2 | +title: "Backend Requirements" |
3 | 3 | description: "Guidance on server-side handling of passkeys registration and authentication" |
4 | 4 | date: 2024-08-13T12:00:00.000Z |
5 | 5 | --- |
6 | 6 |
|
7 | | -The back end drives WebAuthn ceremonies through four primary responsibilities: |
| 7 | +The backend drives WebAuthn ceremonies through four primary responsibilities: |
8 | 8 |
|
9 | 9 | 1. Generate registration options |
10 | 10 | 2. Verify registration responses |
11 | 11 | 3. Generate authentication options |
12 | 12 | 4. Verify authentication responses |
13 | 13 |
|
14 | | -The guidance below is intended to identify best practices to fulfill these responsibilities and |
15 | | -securely incorporate passkeys-based authentication into your website. |
| 14 | +The guidance below identifies best practices to fulfill these responsibilities and securely incorporate passkeys-based authentication into your website. |
| 15 | + |
| 16 | +{{< alert type="info" >}} |
| 17 | +**Please note that this is general guidance; it does not account for any one specific server implementation.** |
| 18 | +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. |
| 19 | +{{< /alert >}} |
| 20 | + |
| 21 | +## Data Structures |
| 22 | + |
| 23 | +A site that uses passkeys should be prepared to store the following "`PasskeyModel`" database model: |
| 24 | + |
| 25 | +```ts |
| 26 | +type PasskeyModel = { |
| 27 | + // SQL: Store as `TEXT`. Index this column |
| 28 | + id: Base64URLString; |
| 29 | + // SQL: Store raw bytes as `BYTEA`/`BLOB`/etc... |
| 30 | + publicKey: Uint8Array; |
| 31 | + // SQL: Foreign Key to an instance of your internal user model |
| 32 | + user: UserModel; |
| 33 | + // SQL: Store as `TEXT`. Index this column. A UNIQUE constraint on |
| 34 | + // (webAuthnUserID + user) also achieves maximum user privacy |
| 35 | + webauthnUserID: Base64URLString; |
| 36 | + // SQL: `INT` or whatever similar type is supported |
| 37 | + counter: number; |
| 38 | + // SQL: `BOOL` or whatever similar type is supported |
| 39 | + backupEligible: boolean; |
| 40 | + // SQL: `BOOL` or whatever similar type is supported |
| 41 | + backupStatus: boolean; |
| 42 | + // SQL: `VARCHAR(255)` and store string array as a CSV string |
| 43 | + // Ex: ble,cable,hybrid,internal,nfc,smart-card,usb |
| 44 | + transports?: AuthenticatorTransport[]; |
| 45 | +}; |
| 46 | +``` |
| 47 | + |
| 48 | +The association between an instance of this `PasskeyModel` and an instance of |
| 49 | +your site's "`UserModel`" should be established in a way that makes sense |
| 50 | +for your site's database architecture. |
| 51 | +For the purposes of the documentation below, the following `UserModel` is assumed: |
| 52 | + |
| 53 | +```ts |
| 54 | +type UserModel = { |
| 55 | + // No assumption is made of how users are assigned IDs within the database |
| 56 | + id: any; |
| 57 | + // An email address, username, or other identifiable information |
| 58 | + username: string; |
| 59 | +}; |
| 60 | +``` |
16 | 61 |
|
17 | | -**Please note that this guidance is not applicable to any specific server implementation.** |
18 | | -It is intended to be a launching point; care should be taken as you consider how best to adapt this |
19 | | -guidance for your particular site. |
20 | 62 |
|
21 | 63 | ## 1. Generate registration options |
22 | 64 |
|
| 65 | +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: |
| 66 | + |
| 67 | +- **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. |
| 68 | +- **User ID:** A unique, random identifier for the user account that the passkey will be registered to. |
| 69 | + This **should not be personally-identifying information** like an email address/username/`UserModel.username`/etc...! |
| 70 | + |
| 71 | +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. |
| 72 | + |
| 73 | +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: |
| 74 | + |
| 75 | +```ts |
| 76 | +/** |
| 77 | + * Generate passkey registration options |
| 78 | + */ |
| 79 | +async function generateRegistrationOptions( |
| 80 | + currentUser: UserModel, |
| 81 | +): PublicKeyCredentialCreationOptionsJSON { |
| 82 | + // A domain name for your site (e.g. "passkeys.dev") |
| 83 | + const rpID: string = process.env.RP_ID; |
| 84 | + // A human-readable name for your website (e.g. "Passkeys Developer Resources") |
| 85 | + const rpName: string = process.env.RP_NAME; |
| 86 | + |
| 87 | + // Generate one-time-use random bytes for the authenticator to sign |
| 88 | + const challenge: Uint8Array = await pseudocodeGenerateChallenge(currentUser); |
| 89 | + // Generate or retrieve a pseudonymous, WebAuthn-specific user identifier as random bytes |
| 90 | + const userID: Uint8Array = await pseudocodeGetWebAuthnUserID(currentUser); |
| 91 | + // Get a list of the user's currently registered passkeys to prevent re-registration |
| 92 | + const userCurrentPasskeys: PasskeyModel[] = await pseudocodeGetCurrentPasskeys(currentUser); |
| 93 | + |
| 94 | + return { |
| 95 | + rp: { |
| 96 | + id: rpID, |
| 97 | + name: rpName, |
| 98 | + }, |
| 99 | + user: { |
| 100 | + id: pseudocodeBytesToBase64URLString(userID), |
| 101 | + name: currentUser.username, |
| 102 | + displayName: '', |
| 103 | + }, |
| 104 | + challenge: pseudocodeBytesToBase64URLString(challenge), |
| 105 | + pubKeyCredParams: [ |
| 106 | + { alg: -8, type: 'public-key' }, |
| 107 | + { alg: -7, type: 'public-key' }, |
| 108 | + { alg: -257, type: 'public-key' }, |
| 109 | + ], |
| 110 | + attestation: 'direct', |
| 111 | + excludeCredentials: userCurrentPasskeys.map((passkey) => ({ |
| 112 | + id: passkey.id, |
| 113 | + transports: passkey.transports, |
| 114 | + type: 'public-key', |
| 115 | + })), |
| 116 | + authenticatorSelection: { |
| 117 | + residentKey: 'preferred', |
| 118 | + userVerification: 'preferred', |
| 119 | + }, |
| 120 | + }; |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +These options should then be remembered since some values will be needed to verify the registration |
| 125 | +response generated for these options: |
| 126 | + |
| 127 | +```ts |
| 128 | +const regOptions = await generateRegistrationOptions(currentUser); |
| 129 | + |
| 130 | +await pseudocodeSaveCurrentRegistrationOptions(currentUser, regOptions); |
| 131 | +``` |
| 132 | + |
| 133 | +`regOptions` can then be transmitted to the frontend (docs "coming soon") as JSON for the frontend |
| 134 | +to eventually pass them in to `navigator.credentials.create()`. |
| 135 | + |
23 | 136 | ## 2. Verify registration responses |
24 | 137 |
|
25 | 138 | ## 3. Generate authentication options |
26 | 139 |
|
27 | 140 | ## 4. Verify authentication responses |
| 141 | + |
| 142 | +## Third-Party Libraries |
| 143 | + |
| 144 | +Many third-party libraries exist to simplify the job of generating options and verifying responses. |
| 145 | +These libraries can help reduce your maintenance burden of supporting passkeys by virtue of |
| 146 | +their ability to keep up with WebAuthn API changes. Head over to |
| 147 | +{{< link "../tools-libraries/libraries" >}}Tools & Libraries > Libraries{{< /link >}} |
| 148 | +for a list of maintained libraries in your backend's language. |
0 commit comments