Back to blog
·8 min read·BitAtlas Team

Passwordless Authentication with Client-Side Key Derivation

Building secure passwordless systems using client-side key derivation and modern cryptographic primitives like PBKDF2 and Argon2.

key derivationpasswordless authclient-sidePBKDF2Argon2

Traditional password-based authentication has dominated the web for decades, but it introduces security burden on both users and systems. Passwordless authentication eliminates this friction by replacing passwords with cryptographic proofs. Client-side key derivation is a powerful technique that enables users to prove possession of a secret without ever transmitting it to the server.

The Problem with Passwords

Password-based authentication creates a security paradox: users must remember strong, unique passwords for every service, yet remembering them encourages weak reuse and poor practices. Servers bear the burden of securely storing password hashes and managing breach risk. And users face the constant threat of phishing attacks that trick them into surrendering their credentials.

Passwordless authentication solves this by shifting from "something you know" to "something you have" or "something you are." But how do we make this work securely and conveniently?

Client-Side Key Derivation: The Bridge

Client-side key derivation allows your application to transform a user-supplied secret (or a recovery phrase) into a cryptographic key in the browser, never transmitting the original secret to the server. The server sees only the derived key or a cryptographic proof of its possession.

Here's the flow:

  1. User provides input — a passphrase, biometric unlock, or recovery phrase
  2. Client derives a key — using a key derivation function (KDF) like PBKDF2 or Argon2
  3. Client signs or encrypts a challenge — proving knowledge of the key
  4. Server verifies the proof — without ever seeing the original input

The beauty here is asymmetry: the user's secret stays client-side, but the server gains cryptographic certainty of identity.

PBKDF2: The Workhorse

PBKDF2 (Password-Based Key Derivation Function 2) has been the standard for two decades. It's simple, audited extensively, and available in every language and browser.

import { pbkdf2 } from 'crypto';

async function deriveKey(password, salt, iterations = 600000) {
  return pbkdf2('sha256', password, salt, 32, (err, derivedKey) => {
    if (err) throw err;
    return derivedKey;
  });
}

In the browser with SubtleCrypto:

async function deriveKeyWeb(password, salt) {
  const encoder = new TextEncoder();
  const baseKey = await crypto.subtle.importKey(
    'raw',
    encoder.encode(password),
    'PBKDF2',
    false,
    ['deriveKey']
  );

  return crypto.subtle.deriveKey(
    {
      name: 'PBKDF2',
      salt: salt,
      iterations: 600000,
      hash: 'SHA-256'
    },
    baseKey,
    { name: 'HMAC', hash: 'SHA-256' },
    true,
    ['sign', 'verify']
  );
}

Key parameters:

  • Iterations: Tune this to your hardware. Modern guidance suggests 100,000 to 1,000,000 — higher makes cracking more expensive.
  • Salt: A random value unique to each user. Store this alongside the user record — it doesn't need to be secret.
  • Hash function: SHA-256 is standard; SHA-512 is also acceptable.

Argon2: The Modern Choice

Argon2 won the Password Hashing Competition (2015) for good reasons: it resists GPU and ASIC attacks by requiring significant memory in addition to computation. This makes brute-force attacks orders of magnitude more expensive.

Unfortunately, SubtleCrypto doesn't support Argon2 natively (yet). Use libsodium.js or tweetnacl.js:

import sodium from 'libsodium.js';

async function deriveKeyArgon2(password, salt) {
  await sodium.ready;

  return sodium.crypto_pwhash(
    32, // output length in bytes
    password,
    salt,
    sodium.crypto_pwhash_OPSLIMIT_MODERATE,
    sodium.crypto_pwhash_MEMLIMIT_MODERATE,
    sodium.crypto_pwhash_ALG_DEFAULT
  );
}

Argon2 parameters:

  • opslimit: CPU cost. OPSLIMIT_MODERATE is a good balance for user-facing flows.
  • memlimit: RAM cost in bytes. MEMLIMIT_MODERATE requires ~64 MB.

The memory requirement is critical: it prevents attackers from parallelizing attacks on commodity hardware.

Building a Passwordless Flow

Here's a complete example using client-side key derivation:

// Client: Register
async function register(email, passphrase) {
  const salt = crypto.getRandomValues(new Uint8Array(16));
  const derivedKey = await deriveKeyWeb(passphrase, salt);

  // Export the derived key as raw bytes
  const keyBytes = await crypto.subtle.exportKey('raw', derivedKey);

  // Sign a challenge with the derived key
  const challenge = new TextEncoder().encode(email + Date.now());
  const signature = await crypto.subtle.sign(
    'HMAC',
    derivedKey,
    challenge
  );

  // Send to server
  await fetch('/register', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email,
      salt: Array.from(new Uint8Array(salt)),
      publicKey: Array.from(new Uint8Array(keyBytes)), // For verification
      signature: Array.from(new Uint8Array(signature))
    })
  });
}

// Server: Verify registration
app.post('/register', (req, res) => {
  const { email, salt, publicKey, signature } = req.body;

  // Store the public key and salt for this user
  db.users.insert({
    email,
    salt: Buffer.from(salt),
    publicKey: Buffer.from(publicKey)
  });

  res.json({ success: true });
});

// Client: Authenticate
async function authenticate(email, passphrase) {
  // Fetch salt from server
  const { salt } = await fetch(`/salt/${email}`).then(r => r.json());
  const saltBytes = new Uint8Array(salt);

  // Re-derive the key from passphrase + salt
  const derivedKey = await deriveKeyWeb(passphrase, saltBytes);

  // Sign a fresh challenge
  const challenge = new TextEncoder().encode(email + Date.now());
  const signature = await crypto.subtle.sign('HMAC', derivedKey, challenge);

  // Authenticate
  const response = await fetch('/authenticate', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      email,
      challenge: Array.from(new Uint8Array(challenge)),
      signature: Array.from(new Uint8Array(signature))
    })
  });

  return response.json();
}

// Server: Verify authentication
app.post('/authenticate', async (req, res) => {
  const { email, challenge, signature } = req.body;
  const user = db.users.findOne({ email });

  const publicKey = await crypto.subtle.importKey(
    'raw',
    Buffer.from(user.publicKey),
    { name: 'HMAC', hash: 'SHA-256' },
    false,
    ['verify']
  );

  const isValid = await crypto.subtle.verify(
    'HMAC',
    publicKey,
    Buffer.from(signature),
    Buffer.from(challenge)
  );

  if (isValid) {
    // Issue session token
    res.json({ sessionToken: generateToken() });
  } else {
    res.status(401).json({ error: 'Authentication failed' });
  }
});

Security Considerations

  1. Salt uniqueness: Use a cryptographically random salt for each user. Never reuse.

  2. Iteration counts: Higher is more secure but slower. Benchmark on your target devices; aim for 100–500 milliseconds on modern phones.

  3. Timing attacks: Use constant-time comparison for signatures. Node.js provides crypto.timingSafeEqual().

  4. Transmission: Always use HTTPS. The derived key is cryptographic material — treat it with the same care as a private key.

  5. Key stretching: If users derive keys from biometrics, add a stretching step like Argon2 before use.

  6. Recovery codes: Passwordless systems need a fallback. Generate and encrypt recovery codes server-side, store them encrypted client-side.

Performance Trade-offs

  • PBKDF2: Fast for the user (100–200 ms), but also fast for attackers. Use 600,000+ iterations.
  • Argon2: Slower for users (500–1000 ms), but exponentially slower for attackers due to memory cost. Recommended.

For user registration and periodic re-authentication (e.g., sensitive operations), Argon2's slower speed is acceptable. For login on every request, cache the derived key in memory or IndexedDB (encrypted) to avoid re-deriving on every access.

The Future of Passwordless

Client-side key derivation is one pillar of passwordless auth. Combine it with:

  • WebAuthn/FIDO2: Hardware security keys and platform biometrics
  • Decentralized identity: Self-sovereign credentials and verifiable claims
  • Zero-knowledge proofs: Prove attributes without revealing underlying data

The transition from passwords to cryptographic proofs is underway. Starting with client-side key derivation gives your application security foundations that scale to more advanced schemes.


Further reading:

Encrypt your agent's data today

BitAtlas gives your AI agents AES-256-GCM encrypted storage with zero-knowledge guarantees. Free tier, no credit card required.