Remote Work Tools

How to Implement Hardware Security Keys for Remote Team Authentication

Deploy YubiKey or similar FIDO2-compatible hardware keys for remote team authentication using WebAuthn—providing phishing resistance and eliminating shared secrets. Hardware security keys represent the strongest defense against credential-based attacks because they store cryptographic keys in tamper-resistant hardware that never leaves employees’ possession. This guide walks through implementing hardware security keys using FIDO2/WebAuthn standards, server-side integration, and practical rollout strategies for distributed teams.

Understanding the Security Model

Hardware security keys implement the FIDO2 (Fast Identity Online 2) protocol, which combines the CTAP2 (Client to Authenticator Protocol 2) specification with WebAuthn. The architecture solves several problems common to password-based and even TOTP-based authentication:

When a user registers a hardware key, the device generates a new key pair. The public key goes to your server, while the private key stays in the hardware. Authentication requires physical presence—the user must touch the key to prove they’re there.

Server-Side Implementation

Most modern authentication frameworks support WebAuthn natively. Here’s how to implement registration and authentication in a Node.js environment using the @simplewebauthn/server library.

Registration Flow

When a user wants to add a hardware key, your server first generates challenge options:

import { generateRegistrationOptions } from '@simplewebauthn/server';

async function startRegistration(user) {
  const options = generateRegistrationOptions({
    rpName: 'Your Company',
    rpID: 'yourdomain.com',
    userID: user.id,
    userName: user.email,
    attestationType: 'direct',
    supportedAlgorithmIDs: [-7, -257],
  });

  // Store the challenge temporarily
  await storeChallenge(user.id, options.challenge);

  return options;
}

The browser passes these options to the hardware key via the WebAuthn API:

const credential = await navigator.credentials.create({
  publicKey: registrationOptionsFromServer
});

Verify the response on your server:

import { verifyRegistrationResponse } from '@simplewebauthn/server';

async function completeRegistration(user, response) {
  const expectedChallenge = await getStoredChallenge(user.id);

  const verification = await verifyRegistrationResponse({
    response,
    expectedChallenge,
    expectedOrigin: 'https://yourdomain.com',
    expectedRPID: 'yourdomain.com',
  });

  if (verification.verified) {
    // Store the credential for future authentication
    await saveCredential(user.id, verification.registrationInfo);
  }

  return verification;
}

Authentication Flow

Authentication follows a similar pattern but uses the stored credential:

import { generateAuthenticationOptions,
         verifyAuthenticationResponse } from '@simplewebauthn/server';

async function startAuthentication(user) {
  const credentials = await getCredentials(user.id);

  const options = generateAuthenticationOptions({
    allowCredentials: credentials.map(cred => ({
      id: cred.credentialID,
      type: 'public-key',
    })),
    userVerification: 'preferred',
  });

  await storeChallenge(user.id, options.challenge);
  return options;
}

Handle the authentication response:

async function completeAuthentication(user, response) {
  const credential = await getCredential(response.credential.id);
  const expectedChallenge = await getStoredChallenge(user.id);

  const verification = await verifyAuthenticationResponse({
    response,
    expectedChallenge,
    expectedOrigin: 'https://yourdomain.com',
    expectedRPID: 'yourdomain.com',
    credential,
  });

  if (verification.verified) {
    // Update counter to detect key cloning attempts
    await updateCredentialCounter(
      credential.id,
      verification.authenticationInfo.newCounter
    );
  }

  return verification;
}

Client-Side Integration

The frontend needs minimal code since the browser handles most WebAuthn interactions:

async function registerKey() {
  const options = await fetch('/auth/webauthn/register/start', {
    method: 'POST'
  }).then(r => r.json());

  try {
    const credential = await navigator.credentials.create({
      publicKey: options
    });

    await fetch('/auth/webauthn/register/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credential)
    });

    console.log('Hardware key registered successfully');
  } catch (error) {
    console.error('Registration failed:', error);
  }
}

async function authenticateWithKey() {
  const options = await fetch('/auth/webauthn/login/start', {
    method: 'POST'
  }).then(r => r.json());

  try {
    const credential = await navigator.credentials.get({
      publicKey: options
    });

    const result = await fetch('/auth/webauthn/login/complete', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(credential)
    });

    if (result.ok) {
      console.log('Authenticated with hardware key');
    }
  } catch (error) {
    console.error('Authentication failed:', error);
  }
}

Rollout Strategy for Remote Teams

Deploying hardware keys to a distributed team requires planning around shipping, enrollment, and backup scenarios.

Phased Rollout

Start with high-risk users: administrators, developers with production access, and anyone with elevated permissions. These users face the greatest threat from credential theft, and they’re typically more comfortable with new technology.

// Example: Check if user is in high-risk group
const HIGH_RISK_ROLES = ['admin', 'devops', 'senior-dev'];

async function requiresHardwareKey(user) {
  return HIGH_RISK_ROLES.includes(user.role);
}

Backup Keys

Every user should register at least two keys—one primary and one backup stored securely (different physical location). Your database schema needs to support multiple credentials per user:

CREATE TABLE auth_credentials (
  id UUID PRIMARY KEY,
  user_id UUID REFERENCES users(id),
  credential_id BYTEA NOT NULL UNIQUE,
  public_key BYTEA NOT NULL,
  counter INTEGER DEFAULT 0,
  device_name VARCHAR(255),
  created_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_user_credentials ON auth_credentials(user_id);

Enrollment Without Physical Presence

For remote teams, ship keys to users before requiring enrollment. Implement a grace period where password authentication remains available while users receive and register their keys:

const ENROLLMENT_GRACE_PERIOD_DAYS = 14;

async function canUsePasswordAuth(user) {
  if (!user.hardwareKeyRequired) return true;

  const enrolled = await hasRegisteredCredentials(user.id);
  if (enrolled) return false;

  const enrollmentDeadline = new Date(
    user.hardwareKeyAssignedAt + ENROLLMENT_GRACE_PERIOD_DAYS * 24 * 60 * 60 * 1000
  );

  return new Date() < enrollmentDeadline;
}

Common Implementation Challenges

Browser compatibility: All modern browsers support WebAuthn, but older browsers need fallbacks. Check window.PublicKeyCredential to detect support.

Key management: Users lose keys. Build administrative interfaces for credential revocation and consider implementing credential migration for users switching between organizations.

Mobile support: Mobile devices can use hardware keys via NFC (most modern phones) or Lightning/USB-C connections. Test thoroughly with your team’s device mix.

Security Considerations

Hardware keys provide strong protection but work best as part of a defense-in-depth strategy. Continue requiring strong passwords, implement session timeouts, and monitor for anomalous authentication patterns. The key advantage is that even if your server is compromised and user passwords are stolen, attackers cannot authenticate without the physical hardware key.

For remote teams specifically, hardware keys eliminate the risk of SMS interception, man-in-the-middle phishing sites, and credential replay attacks that plague traditional authentication methods.


Built by theluckystrike — More at zovo.one