Implement Passwordless Authentication in JavaScript with WebAuthn Level 2 & FIDO2

Mahmut Sarıkaya 4 dk okuma 5 Görüntülenme 0
Implement Passwordless Authentication in JavaScript with WebAuthn Level 2 & FIDO2

Why Passwordless Matters

More than 80% of data breaches in 2023 involved compromised credentials, according to Verizon. Users are tired of remembering complex passwords, and developers are looking for a standard that eliminates phishing vectors. Passwordless authentication, built on public‑key cryptography, offers a way to replace passwords with something the user already possesses—a biometric, a security key, or a trusted device.

Understanding WebAuthn Level 2

WebAuthn Level 2, the latest iteration of the W3C specification, adds support for resident keys, user verification methods, and large‑blob storage. Combined with the FIDO2 alliance, it defines a complete end‑to‑end flow: the client creates a public‑key credential, the server stores the public key, and future authentications verify signatures without ever transmitting a secret.

Key concepts you must know:

  • PublicKeyCredential – the object returned by the browser after registration or authentication.
  • Attestation – optional data that proves the authenticator is genuine.
  • Resident vs. non‑resident keys – whether the credential identifier is stored on the authenticator or the server.

Preparing the Server Side

The server must generate a challenge (a cryptographically random byte array) and a rp (relying party) object. In Node.js you can use the fido2-lib package, which abstracts most of the heavy lifting.

const {Fido2Lib}=require("fido2-lib");
const fido2=new Fido2Lib({
  timeout:60000,
  rpId:"example.com",
  rpName:"Example Corp",
  challengeSize:64,
  attestation:"none"
});
// Generate registration options
async function getRegistrationOptions(user){
  return await fido2.attestationOptions({
    user,
    authenticatorSelection:{
      residentKey:"required",
      userVerification:"preferred"
    }
  });
}
// Verify registration response
async function verifyAttestation(response){
  return await fido2.attestationResult(response, {
    rpId:"example.com",
    origin:"https://example.com",
    factor:"either"
  });
}

Store user.id, the generated credentialId, and the public key (in authenticatorData) in your database. These values are needed for the authentication step.

Client‑Side Registration Flow

On the front end, the process consists of three steps: fetch the challenge, call navigator.credentials.create(), and send the attestation back to the server.

async function register(){
  const resp=await fetch("/register/options");
  const options=await resp.json();
  // Convert base64url strings to Uint8Array
  options.challenge=Uint8Array.from(atob(options.challenge),c=>c.charCodeAt(0));
  options.user.id=Uint8Array.from(atob(options.user.id),c=>c.charCodeAt(0));
  const cred=await navigator.credentials.create({publicKey:options});
  const attestation={
    id:cred.id,
    rawId:Array.from(new Uint8Array(cred.rawId)),
    type:cred.type,
    response:{
      clientDataJSON:Array.from(new Uint8Array(cred.response.clientDataJSON)),
      attestationObject:Array.from(new Uint8Array(cred.response.attestationObject))
    }
  };
  await fetch("/register/verify",{
    method:"POST",
    headers:{'Content-Type':'application/json'},
    body:JSON.stringify(attestation)
  });
}

Notice the conversion between base64url and Uint8Array—WebAuthn requires binary data, while JSON transports strings. Using Array.from preserves the exact byte sequence, preventing “Invalid JSON” errors on the server.

Client‑Side Authentication Flow

Authentication mirrors registration but uses navigator.credentials.get(). The server supplies a allowCredentials list that contains the previously stored credentialId.

async function login(){
  const resp=await fetch("/login/options");
  const options=await resp.json();
  options.challenge=Uint8Array.from(atob(options.challenge),c=>c.charCodeAt(0));
  options.allowCredentials=options.allowCredentials.map(cred=>({
    type:cred.type,
    id:Uint8Array.from(atob(cred.id),c=>c.charCodeAt(0)),
    transports:cred.transports
  }));
  const assertion=await navigator.credentials.get({publicKey:options});
  const data={
    id:assertion.id,
    rawId:Array.from(new Uint8Array(assertion.rawId)),
    type:assertion.type,
    response:{
      clientDataJSON:Array.from(new Uint8Array(assertion.response.clientDataJSON)),
      authenticatorData:Array.from(new Uint8Array(assertion.response.authenticatorData)),
      signature:Array.from(new Uint8Array(assertion.response.signature)),
      userHandle:assertion.response.userHandle?Array.from(new Uint8Array(assertion.response.userHandle)):null
    }
  };
  await fetch("/login/verify",{
    method:"POST",
    headers:{'Content-Type':'application/json'},
    body:JSON.stringify(data)
  });
}

After the server validates the signature against the stored public key, it creates a session token. From the user’s perspective the login is a single tap or a fingerprint scan—no password field appears.

Practical Tips & Common Pitfalls

1. Browser support matters. As of 2024, Chrome 120+, Edge 120+, and Safari 16.5 implement Level 2 features. Provide a graceful fallback (e.g., OTP) for older browsers.

2. Use HTTPS everywhere. WebAuthn requires a secure context; localhost is an exception for development, but production must be served over TLS.

3. Handle user verification failures. If the authenticator reports userVerified:false, prompt the user to retry or fall back to a secondary factor.

4. Store challenges temporarily. Keep the challenge in a short‑lived cache (e.g., Redis with a 2‑minute TTL) to prevent replay attacks.

5. Test with multiple authenticators. Physical security keys (YubiKey 5 Series), platform authenticators (Windows Hello, Android Fingerprint), and roaming devices behave slightly differently—especially around resident keys.

Conclusion

Implementing passwordless login with WebAuthn Level 2 and FIDO2 in JavaScript eliminates the weakest link in the authentication chain. By generating secure challenges on the server, leveraging navigator.credentials APIs, and persisting public keys, you can offer users a frictionless, phishing‑resistant experience. The code snippets above illustrate a production‑ready flow; adapt them to your stack, respect security best practices, and you’ll be ready for the next generation of web authentication.

Sources

W3C Web Authentication API (WebAuthn) Specification, FIDO Alliance – FIDO2 Overview, Mozilla Developer Network (MDN) – Web Authentication API.

Author: Mahmut Sarıkaya — sarikayadev.com

Etiketler: #WebAuthn #FIDO2 #passwordless login #JavaScript security #public key credentials
Paylaş:
M

Yazar

Mahmut Sarıkaya

yazılım Geliştirici

Yorumlar

Henüz yorum yok. İlk yorumu siz yapın!

Yorum Bırakın

1 + 5 =