WebAuthn / Passkeys
Enable WebAuthn and passkey authentication in HelloJohn — hardware security keys, Touch ID, Face ID, and Windows Hello as MFA or primary auth.
WebAuthn (Web Authentication API) enables authentication using hardware security keys (YubiKey, etc.) and platform authenticators (Touch ID, Face ID, Windows Hello). These are the most phishing-resistant MFA methods available.
WebAuthn vs. Passkeys
| WebAuthn (security keys) | Passkeys | |
|---|---|---|
| Device | External hardware key (YubiKey) | Built-in (Touch ID, Face ID, Windows Hello) |
| Syncs across devices | No | Yes (via iCloud Keychain, Google Password Manager) |
| Phishing resistance | Highest | High |
| Use as 2nd factor | Yes | Yes |
| Use as primary auth | Yes (passwordless) | Yes (passwordless) |
HelloJohn supports both via the same WebAuthn API — the browser determines which authenticator is used.
Browser support
| Browser | Platform authenticators | Security keys |
|---|---|---|
| Chrome 67+ | Yes | Yes |
| Safari 14+ (iOS/macOS) | Yes (Touch ID / Face ID) | Yes |
| Firefox 60+ | Yes | Yes |
| Edge 18+ | Yes (Windows Hello) | Yes |
Enrollment flow
Initiate enrollment
POST /v2/auth/mfa/webauthn/enroll
Authorization: Bearer {access_token}
Content-Type: application/json
{
"authenticator_type": "platform" // "platform" (passkey) or "cross-platform" (security key)
}Response:
{
"enrollment_id": "enroll_01HX...",
"options": {
"challenge": "base64url-encoded-challenge",
"rp": { "id": "yourapp.com", "name": "Your App" },
"user": { "id": "base64url-user-id", "name": "user@example.com", "displayName": "User" },
"pubKeyCredParams": [{ "type": "public-key", "alg": -8 }],
"authenticatorSelection": {
"authenticatorAttachment": "platform",
"residentKey": "preferred",
"userVerification": "preferred"
},
"timeout": 60000
}
}Browser creates the credential
Pass the options to the browser's WebAuthn API:
const credential = await navigator.credentials.create({
publicKey: options
})The browser prompts the user to authenticate (Face ID, fingerprint, PIN, etc.) and creates a public/private key pair. The private key never leaves the device.
Complete enrollment
Send the credential back to HelloJohn:
POST /v2/auth/mfa/webauthn/enroll/complete
Authorization: Bearer {access_token}
Content-Type: application/json
{
"enrollment_id": "enroll_01HX...",
"credential": {
"id": "credential-id",
"rawId": "base64url-raw-id",
"response": {
"clientDataJSON": "base64url...",
"attestationObject": "base64url..."
},
"type": "public-key"
}
}Response 200 OK:
{
"method_id": "mfa_01HX...",
"backup_codes": ["XXXX-XXXX", ...]
}Verification (login challenge)
# Get challenge
POST /v2/auth/mfa/webauthn/challenge
Content-Type: application/json
{
"challenge_id": "challenge_01HX..."
}Response:
{
"options": {
"challenge": "base64url...",
"allowCredentials": [{ "id": "credential-id", "type": "public-key" }],
"timeout": 60000,
"userVerification": "preferred"
}
}Browser gets the assertion:
const assertion = await navigator.credentials.get({ publicKey: options })Submit the assertion:
POST /v2/auth/mfa/webauthn/verify
Content-Type: application/json
{
"challenge_id": "challenge_01HX...",
"assertion": {
"id": "credential-id",
"rawId": "base64url...",
"response": {
"authenticatorData": "base64url...",
"clientDataJSON": "base64url...",
"signature": "base64url..."
},
"type": "public-key"
}
}SDK integration
import { useWebAuthn } from '@hellojohn/react'
function WebAuthnSetup() {
const { enroll, isSupported } = useWebAuthn()
if (!isSupported) {
return <p>Your browser doesn't support passkeys.</p>
}
return (
<button onClick={() => enroll({ type: 'platform' })}>
Set up Face ID / Touch ID
</button>
)
}
function WebAuthnChallenge({ challengeId }: { challengeId: string }) {
const { verify } = useWebAuthn()
return (
<button onClick={() => verify(challengeId)}>
Verify with Face ID / Touch ID
</button>
)
}The SDK handles the full WebAuthn ceremony (options fetch → browser API → assertion submit) in a single call.
Relying Party configuration
Configure the WebAuthn Relying Party in your environment:
WEBAUTHN_RP_ID=yourapp.com # must match your app's domain
WEBAUTHN_RP_NAME=Your App # display name shown in browser prompts
WEBAUTHN_RP_ORIGINS=https://yourapp.com,https://www.yourapp.comWEBAUTHN_RP_ID must exactly match the origin domain. If your app is at app.yourapp.com, set WEBAUTHN_RP_ID=app.yourapp.com. Passkeys created for one RP ID won't work on another.