diff --git a/malta.config.json b/malta.config.json index 2524804..387c0f4 100644 --- a/malta.config.json +++ b/malta.config.json @@ -15,7 +15,7 @@ ["Generating random values", "/random-values"], ["OAuth", "/oauth"], ["Multi-factor authentication (MFA)", "/mfa"], - ["Passkeys", "/passkeys"], + ["WebAuthn", "/webauthn"], ["Cross-site request forgery (CSRF)", "/csrf"], ["Open redirect", "/open-redirect"] ] diff --git a/pages/mfa.md b/pages/mfa.md index dcd8299..095e267 100644 --- a/pages/mfa.md +++ b/pages/mfa.md @@ -11,7 +11,7 @@ title: "Multi-factor authentication (MFA)" - [Generate QR code](#generate-qr-code) - [Validate OTPs](#validate-otps) - [SMS](#sms) -- [WebAuthn (passkeys)](#webauthn-passkeys) +- [WebAuthn](#webauthn) - [Recovery codes](#recovery-codes) ## Overview @@ -91,11 +91,11 @@ We discourage SMS based MFA as it can be intercepted and unreliable at times. Ho Throttling must be implemented. A basic example is blocking attempts for 15 to 60 minutes after the 5th consecutive failed attempt. The user should also be notified to change the password as well. -## WebAuthn (passkeys) +## WebAuthn The [Web Authentication API (WebAuthn)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Authentication_API) allows applications to use user devices for authentication using public key cryptography. You can either verify the user's identity with the devices PIN code or biometrics, or just verify the device. Both works as a second factor and the latter can be more user-friendly as users aren't prompted for their password/fingerprint. -See the [passkeys](/passkeys) guide for implementations. +See the [WebAuthn](/webauthn) guide for implementations. ## Recovery codes diff --git a/pages/password-reset.md b/pages/password-reset.md index 032a56b..dd35460 100644 --- a/pages/password-reset.md +++ b/pages/password-reset.md @@ -39,7 +39,7 @@ The token must be single-use. Delete the token when the user sends a valid passw Make sure to set the [Referrer Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) tag to `strict-origin` (or equivalent) for any path that includes tokens to protect the tokens from referer leakage. -If the user has implemented [multi-factor authentication](/mfa), such as via authenticator apps or passkeys, they should be prompted to authenticate using their second factor before entering their new password. +If the user has implemented [multi-factor authentication](/mfa), such as via authenticator apps or WebAuthn, they should be prompted to authenticate using their second factor before entering their new password. ## Error handling diff --git a/pages/sessions.md b/pages/sessions.md index 4309d5c..2380c05 100644 --- a/pages/sessions.md +++ b/pages/sessions.md @@ -57,7 +57,7 @@ func validateSession(sessionId string) (*Session, error) { ### Sudo mode -An alternative to short-lived sessions is to implement long-lived sessions coupled with sudo mode. Sudo mode allows authenticated users to access security-critical components for a limited time by re-authenticating with one of their credentials (passwords, passkeys, TOTP, etc). A simple way to implement this is by keeping track of when the user last used their credentials in each session. This approach provides the security benefits of short-lived sessions without annoying frequent users. This can also help against [session hijacking](#session-hijacking). +An alternative to short-lived sessions is to implement long-lived sessions coupled with sudo mode. Sudo mode allows authenticated users to access security-critical components for a limited time by re-authenticating with one of their credentials (passwords, WebAuthn credentials, TOTP, etc). A simple way to implement this is by keeping track of when the user last used their credentials in each session. This approach provides the security benefits of short-lived sessions without annoying frequent users. This can also help against [session hijacking](#session-hijacking). ## Session hijacking diff --git a/pages/passkeys.md b/pages/webauthn.md similarity index 76% rename from pages/passkeys.md rename to pages/webauthn.md index 511eb0b..0774d19 100644 --- a/pages/passkeys.md +++ b/pages/webauthn.md @@ -1,8 +1,8 @@ --- -title: "Passkeys" +title: "WebAuthn" --- -# Passkeys +# WebAuthn ## Table of contents @@ -13,9 +13,9 @@ title: "Passkeys" ## Overview -Passkeys are password replacements built on top of public-key cryptography and the [Web Authentication (WebAuthn) standard](https://www.w3.org/TR/webauthn-2/). They allow users to authenticate with their device, either with a PIN code or biometrics. The private key is stored in the user's device, while the public key is stored in your application. Applications can authenticate users by verifying signatures. Since passkeys are bounded to the user's device (or devices) and brute-forcing is impossible, a potential attacker needs physical access to a device. This makes it a much secure alternative to passwords and can be as secure as passwords with 2FA using SMS, emails, or authenticator apps. +The [Web Authentication (WebAuthn) standard](https://www.w3.org/TR/webauthn-2/) allow users to authenticate with their device, either with a PIN code or biometrics. The private key is stored in the user's device, while the public key is stored in your application. Applications can authenticate users by verifying signatures. Since credentials are bounded to the user's device (or devices) and brute-forcing is impossible, a potential attacker needs physical access to a device. -While passkeys are credentials that verify user identity, the same technology (WebAuthn) can be used to check that user has access to their device (user presence). This makes using WebAuthn a great second-factor on top of regular passwords. Hardware security tokens that don't provide pin-code or biometrics authentication can be used here. This page will also cover this usage. +WebAuthn are usually used in 2 ways - with passkeys or security tokens. While they don't have a strict definition, passkeys usually refer to credentials that can replace passwords and stored in the authenticator (resident keys). Security tokens, on the other hand, are meant to be used as a second factor, after authenticating with a password. Credentials for 2FA are usually encrypted and stored in the relying party's server. In both cases, they are a more secure alternatives to existing methods. Using WebAuthn, applications can also verify the device with the manufacture. This requires attestation and is not covered in this page. @@ -26,6 +26,7 @@ Using WebAuthn, applications can also verify the device with the manufacture. Th - Challenge: A randomly generated, single-use [token](/server-side-tokens) to prevent replay attacks. The recommended minimum entropy is 16 bytes. - User presence: User has access to the device. - User verification: User has verified their identity via a pin-code or biometrics. +- Resident keys, discoverable credentials: Credentials stored in stored in authenticators (user devices and security tokens). Non-resident keys are encrypted and stored in relying party servers (your database). ## Registration @@ -52,8 +53,18 @@ const credential = await navigator.credentials.create({ ], challenge, authenticatorSelection: { + // See note below. userVerification: "required", + residentKey: "required", + requireResidentKey: true, }, + // list of existing credentials + excludeCredentials: [ + { + id: new Uint8Array(/*...*/), + type: "public-key", + }, + ], }, }); if (!(credential instanceof PublicKeyCredential)) { @@ -72,12 +83,42 @@ const attestationObject: ArrayBuffer = response.attestationObject; - `user.id`: Random user ID for the authenticator. This can be different from the actual user ID your application uses. - `user.name`: A human-friendly user identifier (username, email). - `user.displayName`: A human-friendly display name (does not need to be unique). +- `excludeCredentials`: A list of the user's credentials to avoid duplicate credentials. The algorithm ID is from the [IANA COSE Algorithms registry](https://www.iana.org/assignments/cose/cose.xhtml). ECDSA with SHA-256 (ES256) is recommended as it is widely supported. You can also pass `-257` for RSASSA-PKCS1-v1.5 (RS256) to support a wider range of devices but devices that only support it are rare. For most cases, `attestation` should be set to `"none"`. We don't need to verify of the authenticator and not all authenticators support it. -For passkeys, `userVerification` should be set to `"required"`. This ensures that the authenticator prompts the user for the pin code or fingerprint. For using WebAuthn as a second-factor, where you just need to check that user has the device, set this is `"preferred"` or even `"discouraged"`. +For passkeys, ensure the public key is a resident key and requires user verification. + +```ts +const credential = await navigator.credentials.create({ + publicKey: { + // ... + authenticatorSelection: { + userVerification: "required", + residentKey: "required", + requireResidentKey: true, + }, + }, +}); +``` + +For security tokens, we can skip user verification and the credential doesn't need to be a resident key. We can limit the authenticator to security tokens by setting `authenticatorAttachment` to `cross-platform` as well. + +```ts +const credential = await navigator.credentials.create({ + publicKey: { + // ... + authenticatorSelection: { + userVerification: "discouraged", + residentKey: "discouraged", + requireResidentKey: false, + authenticatorAttachment: "cross-platform", + }, + }, +}); +``` The client data JSON and authenticator data are sent to the server for verification. A simple way to send binary data is by encoding it with base64. Another option is use schemes like CBOR that encode JSON-like data into binary. @@ -246,6 +287,24 @@ const signature: ArrayBuffer = response.signature); const credentialId: ArrayBuffer = publicKeyCredential.rawId; ``` +For implementing 2FA with security tokens, pass a list of the user's credentials to `allowCredentials` to support non-resident keys. + +```ts +const credential = await navigator.credentials.get({ + publicKey: { + challenge, + userVerification: "required", + // list of user credentials + allowCredentials: [ + { + id: new Uint8Array(/*...*/), + type: "public-key", + }, + ], + }, +}); +``` + The client data, authenticator data, signature, and credential ID are sent to the server. The challenge, the authenticator, and the client data are first verified. This part is nearly identical to the steps for verifying attestation expect that the client data type should be `webauthn.get`. Another difference is that the credential portion of the authenticator is not included.