Passkeyssecurity

Why Passkeys

Phishing-resistant authentication without passwords

v1.2·9 min read·Kenneth Pernyér
securityauthenticationfido2webauthnpasswordlessjwtgrpc

The Problem

Passwords are broken. Users reuse them across sites. They're phishable. They're stored in databases that get breached. Even with hashing and salting, password authentication is a liability.

Two-factor authentication helps but doesn't solve the problem.

SMS codes are intercepted. TOTP codes are phishable with real-time proxies. Push notifications are approved by confused users. Every 2FA method that requires users to enter something can be socially engineered.

We needed authentication that:

  • Cannot be phished (cryptographically impossible)
  • Doesn't rely on user-remembered secrets
  • Works across devices seamlessly
  • Meets enterprise security requirements

Current Options

OptionProsCons
Passwords + 2FATraditional password with second factor.
  • Familiar to users
  • 2FA adds meaningful security
  • Widely supported
  • Still phishable with sophisticated attacks
  • Password reuse remains common
  • User friction reduces adoption
  • Support burden for password resets
Magic LinksEmail-based authentication without passwords.
  • No password to remember or steal
  • Simple user experience
  • Email becomes the security boundary
  • Email account compromise means full compromise
  • Slow authentication flow
  • Email deliverability issues
  • Not suitable for high-frequency auth
Passkeys (FIDO2/WebAuthn)Cryptographic authentication bound to device.
  • Phishing-proof by design
  • Nothing to remember or type
  • Fast authentication (biometric)
  • Cross-device sync available
  • Requires device with passkey support
  • Recovery mechanisms needed
  • Not universally supported yet
  • User education required

Future Outlook

Passkeys are the end of passwords.

Platform support is now universal.

Apple, Google, and Microsoft all support passkeys with cross-device sync. Every major browser implements WebAuthn. The infrastructure is in place.

Adoption is accelerating. Major services (Google, Apple, Microsoft, PayPal, eBay) support passkeys. Enterprise identity providers are adding support. The tipping point is near.

The future is passwordless by default. Passwords will become a legacy fallback, like security questions today. New users will never create passwords; they'll register with passkeys from the start.

Our Decision

Why we chose this

  • Phishing-proofCryptographic binding to origin means fake sites can't capture credentials
  • Nothing to rememberBiometric or PIN authenticates; no passwords to forget
  • Fast authenticationTouch ID or Face ID is faster than typing passwords
  • No credential databasesServer stores public keys only; breaches don't expose secrets

×Trade-offs we accept

  • Device dependencyNeed device with passkey support; recovery planning required
  • User educationConcept is new; users need guidance
  • Not universal yetSome browsers and devices don't support passkeys

Motivation

Password-based authentication is a liability we refuse to carry. Every password in our system is a potential breach vector—whether from reuse, phishing, or database compromise.

Passkeys eliminate the entire category of password-related attacks. There's no password to phish, no password to breach, no password to guess. Authentication becomes cryptographic verification.

For business systems, this isn't optional security—it's baseline. The cost of a credential compromise far exceeds the cost of implementing proper authentication.

Recommendation

Implement passkeys as the entry point to Converge's Zero Trust chain:

Web Client Flow:

  1. User authenticates with passkey (WebAuthn)
  2. Server verifies → issues short-lived JWT
  3. JWT used for gRPC-Web calls to backend services
  4. gRPC interceptors validate JWT on every call

Implementation:

  • Frontend: WebAuthn API via @simplewebauthn/browser
  • Auth service: Firebase Auth with passkey support, or custom with @simplewebauthn/server
  • Token exchange: Passkey verification → JWT with user claims + permissions
  • Backend validation: gRPC interceptor validates JWT (see Zero Trust article)

Recovery:

  • Backup passkeys on multiple devices
  • Recovery codes (one-time use, stored encrypted)
  • Trusted device registration for account recovery

For enterprise customers: support SAML/OIDC federation with their IdP, which can require passkeys on their end.

Examples

auth/passkey-auth.tstypescript
import {
  verifyAuthenticationResponse,
  generateAuthenticationOptions,
} from '@simplewebauthn/server';
import { SignJWT } from 'jose';

const rpID = 'converge.zone';
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);

// Step 1: Generate authentication challenge
export async function startAuthentication(userId: string) {
  const credentials = await getUserCredentials(userId);

  return generateAuthenticationOptions({
    rpID,
    allowCredentials: credentials.map(c => ({
      id: c.credentialId,
      type: 'public-key',
    })),
    userVerification: 'required',
  });
}

// Step 2: Verify passkey and issue JWT for gRPC calls
export async function completeAuthentication(
  userId: string,
  response: AuthenticationResponseJSON
): Promise<{ jwt: string; expiresAt: number }> {
  const credential = await getCredential(response.id);
  const challenge = await getStoredChallenge(userId);

  const verification = await verifyAuthenticationResponse({
    response,
    expectedChallenge: challenge,
    expectedOrigin: 'https://converge.zone',
    expectedRPID: rpID,
    credential,
  });

  if (!verification.verified) {
    throw new Error('Authentication failed');
  }

  // Issue short-lived JWT for gRPC backend calls
  const expiresAt = Math.floor(Date.now() / 1000) + 3600; // 1 hour
  const jwt = await new SignJWT({
    sub: userId,
    permissions: await getUserPermissions(userId),
  })
    .setProtectedHeader({ alg: 'HS256' })
    .setExpirationTime(expiresAt)
    .setIssuedAt()
    .sign(JWT_SECRET);

  return { jwt, expiresAt };
}

Passkey verification issues a JWT. The JWT is sent with every gRPC-Web request. Backend gRPC interceptors validate it (see Zero Trust article).

client/grpc-client.tstypescript
import { GrpcWebFetchTransport } from '@protobuf-ts/grpcweb-transport';

// gRPC-Web client with passkey-issued JWT
export function createAuthenticatedClient(jwt: string) {
  const transport = new GrpcWebFetchTransport({
    baseUrl: 'https://api.converge.zone',
    meta: {
      // JWT from passkey auth attached to every call
      authorization: `Bearer ${jwt}`,
    },
  });

  return new ConvergeServiceClient(transport);
}

// Usage after passkey login
const { jwt } = await completeAuthentication(userId, webauthnResponse);
const client = createAuthenticatedClient(jwt);

// Every gRPC call now carries verified user identity
const response = await client.processOrder({ orderId: '123' });

After passkey authentication, the JWT is attached to all gRPC-Web requests. The backend Zero Trust interceptor validates identity on every call.

Related Articles

Stockholm, Sweden

Version 1.2

Kenneth Pernyér signature