Passwordless Authentication


The Passwordless Vision

Passwords are the weakest link in authentication. Passwordless authentication eliminates them entirely, replacing secrets with cryptographic keys.

WebAuthn and FIDO2

Web Authentication (WebAuthn) is a W3C standard for public-key credential authentication:




// Registration


async function registerPasskey() {


const credential = await navigator.credentials.create({


publicKey: {


challenge: new Uint8Array([/* server-generated challenge */]),


rp: {


id: "example.com",


name: "Example Corp"


},


user: {


id: new TextEncoder().encode("user-123"),


name: "alice@example.com",


displayName: "Alice"


},


pubKeyCredParams: [


{ type: "public-key", alg: -7 }, // ES256


{ type: "public-key", alg: -257 } // RS256


],


authenticatorSelection: {


authenticatorAttachment: "platform",


residentKey: "required",


userVerification: "required"


}


}


});




// Send to server


await fetch("/api/auth/passkey/register", {


method: "POST",


body: JSON.stringify({


id: credential.id,


rawId: arrayBufferToBase64(credential.rawId),


type: credential.type,


response: {


clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),


attestationObject: arrayBufferToBase64(credential.response.attestationObject)


}


})


});


}





Server-Side Verification




from webauthn import generate_registration_options, verify_registration_response


from webauthn.helpers.structs import RegistrationCredential




def start_registration(user):


options = generate_registration_options(


rp_id="example.com",


rp_name="Example Corp",


user_id=str(user.id).encode(),


user_name=user.email,


user_display_name=user.name


)




# Store challenge temporarily


cache.set(f"webauthn:challenge:{user.id}", options.challenge, time=300)




return options




def complete_registration(user, credential_data):


credential = RegistrationCredential(


id=credential_data["id"],


raw_id=credential_data["rawId"],


type=credential_data["type"],


response={


"client_data_json": credential_data["response"]["clientDataJSON"],


"attestation_object": credential_data["response"]["attestationObject"]


}


)




verification = verify_registration_response(


credential=credential,


expected_challenge=cache.get(f"webauthn:challenge:{user.id}"),


expected_rp_id="example.com",


expected_origin="https://example.com"


)




# Store credential for future logins


store_credential(user.id, verification.credential_id, verification.public_key)





Authentication Flow




// Login with passkey


async function authenticateWithPasskey() {


const credential = await navigator.credentials.get({


publicKey: {


challenge: new Uint8Array([/* server challenge */]),


rpId: "example.com",


userVerification: "required"


}


});




const response = await fetch("/api/auth/passkey/authenticate", {


method: "POST",


body: JSON.stringify({


id: credential.id,


rawId: arrayBufferToBase64(credential.rawId),


type: credential.type,


response: {


clientDataJSON: arrayBufferToBase64(credential.response.clientDataJSON),


authenticatorData: arrayBufferToBase64(credential.response.authenticatorData),


signature: arrayBufferToBase64(credential.response.signature),


userHandle: credential.response.userHandle


? arrayBufferToBase64(credential.response.userHandle)


: null


}


})


});




if (response.ok) window.location.href = "/dashboard";


}





Magic Links

For devices without platform authenticators:




import secrets


from datetime import datetime, timedelta




def send_magic_link(email):


token = secrets.token_urlsafe(32)


expiry = datetime.utcnow() + timedelta(minutes=15)




# Store token


cache.set(f"magic_link:{token}", email, time=900)




# Send email


link = f"https://example.com/auth/magic?token={token}"


send_email(email, "Your login link", f"Click: {link}")




def verify_magic_link(token):


email = cache.get(f"magic_link:{token}")


if email:


cache.delete(f"magic_link:{token}")


return create_session(email)


return None





Conclusion

Passwordless authentication improves both security and UX. Use WebAuthn with platform authenticators as the primary method, fall back to magic links for cross-device scenarios. Store public keys for verification and never handle private keys server-side. Passkeys sync across devices via platform providers, making them the most practical passwordless solution for 2026.