Home Knowledge Base WP 2FA WP 2FA Passkeys (WebAuthn) REST API Documentation

WP 2FA Passkeys (WebAuthn) REST API Documentation

This documentation covers WP 2FA’s core REST API endpoint for validating a user’s second-factor authentication token, including TOTP, email codes, backup codes, SMS codes, and other supported token-based providers.

Note: depending on configuration, you may still need to complete additional 2FA steps after passkey sign-in. For token-based verification flows, refer to the WP 2FA REST API documentation.

Namespace: wp-2fa-passkeys/v1

How it works: The plugin bundles @simplewebauthn/browser and calls the API with WordPress’s wp.apiFetch(), which handles authentication automatically via cookies and nonces — no Bearer tokens or API keys. startRegistration() and startAuthentication() convert the challenge and binary fields to and from base64url for you, so you rarely touch encoding directly.

Key rules

  • All binary fields (challenges, rawId, clientDataJSON, attestationObject, etc.) are base64url-encoded (RFC 4648 §5): characters A-Z a-z 0-9 - _, no + or /.
  • Challenges are short-lived (sign-in challenges expire after 60 seconds).
  • Registration endpoints require an authenticated user; sign-in endpoints are public.
  • All passkey endpoints are POST.

Endpoint summary

FlowEndpointAuth
Registration – request challenge/register/requestRequired
Registration – complete/register/responseRequired
Revoke a passkey/register/revokeRequired
Enable / disable a passkey/register/enableRequired
Sign-in – request challenge/singin/requestPublic
Sign-in – complete/singin/responsePublic
Note: the sign-in path is spelled singin (not signin) in the plugin. Use it exactly as shown.

Registration flow

Registration is two calls: request a challenge, then send the authenticator’s response back to verify it.

1. Request challenge

POST: /wp-json/wp-2fa-passkeys/v1/register/request

Optional body: is_usb (boolean) – whether to target a cross-platform/USB authenticator. Returns a publicKeyCredentialCreationOptions object to pass straight into startRegistration().

2. Complete registration

POST: /wp-json/wp-2fa-passkeys/v1/register/response

ParameterTypeRequiredDescription
attRespobjectYesThe attestation object returned by startRegistration().
passkey_namestringNoFriendly label for the passkey. If omitted, a name is generated from the platform.

Success and error responses:

// Success
{ "status": "verified", "message": "Successfully registered." }
// Error
{ "code": "public_key_validation_failed",
  "message": "Public key validation failed.",
  "data": { "status": 400 } }

Minimal registration example

// Step 1 - get the challenge
const options = await wp.apiFetch( {
  path: '/wp-2fa-passkeys/v1/register/request',
  method: 'POST',
  data: { is_usb: false },
} );
// Step 2 - let the browser create the credential
const attResp = await startRegistration( options );
// Step 3 - send it back to finish
const res = await wp.apiFetch( {
  path: '/wp-2fa-passkeys/v1/register/response',
  method: 'POST',
  data: { attResp, passkey_name: 'My laptop' },
} );
if ( res.status === 'verified' ) {
  window.location.reload();
}

Revoke a passkey

POST: /wp-json/wp-2fa-passkeys/v1/register/revoke

Body: fingerprint (string, required) – the base64url credential ID to delete.

// Success
{ "status": "success", "message": "Successfully revoked." }

Enable / disable a passkey

POST: /wp-json/wp-2fa-passkeys/v1/register/enable

Toggles a passkey’s enabled state without deleting it. The caller must be the credential owner or have the edit_user capability.

ParameterTypeRequiredDescription
info.fingerprintstringYesBase64url credential ID.
info.user_idintegerYesID of the user who owns the credential.
// Success (status toggles between enabled and disabled)
{ "status": "success", "message": "Successfully enabled/disabled." }

Sign-in flow

Sign-in mirrors registration: request a challenge for the username, get an assertion from the authenticator, then send it back to complete the login.

1. Request challenge

POST/wp-json/wp-2fa-passkeys/v1/singin/request

Body: user (string, required) – username or email. The response holds an options object (challenge, allowCredentials, etc.) plus a request_id you must echo back in step 2. The challenge is stored server-side with a 60-second TTL.

2. Complete authentication

POST: /wp-json/wp-2fa-passkeys/v1/singin/response

ParameterTypeRequiredDescription
request_idstringYesThe ID returned by the challenge request.
asseRespobjectYesThe assertion returned by startAuthentication().
userstringYesUsername or email address.
redirect_tostringNoURL to redirect to after a successful sign-in.
// Success
{ "status": "verified",
  "message": "Successfully signin with Passkey.",
  "redirect_to": "https://example.com/wp-admin/" }
// Error
{ "code": "invalid_credentials",
  "message": "Invalid credentials.",
  "data": { "status": 400 } }

On success, the endpoint sets the WordPress auth cookie via wp_set_auth_cookie() and updates the credential’s last_used timestamp. Depending on the configuration, the user may still need to complete additional 2FA steps.

Minimal sign-in example

// Step 1 - challenge for this user
const { options, request_id } = await wp.apiFetch( {
  path: '/wp-2fa-passkeys/v1/singin/request',
  method: 'POST',
  data: { user: username },
} );
// Step 2 - get the assertion from the authenticator
const asseResp = await startAuthentication( options );
// Step 3 - complete the login
const res = await wp.apiFetch( {
  path: '/wp-2fa-passkeys/v1/singin/response',
  method: 'POST',
  data: { request_id, asseResp, user: username, redirect_to: redirectTo },
} );
if ( res.status === 'verified' ) {
  window.location.href = res.redirect_to || '/wp-admin';
}

Storage

Passkey credentials are stored in WordPress user meta. Each credential lives under its own key and records the device name, timestamps, enabled flag, and the public key (the private key never leaves the user’s device).

ItemStorageKey patternTTL
CredentialUser metawp_2fa_passkey_<credential_id>Persistent
Registration challengeUser metawp_2fa_passkey_challengeUntil used
Sign-in challengeTransientwp_2fa_passkey_<request_id>60 seconds

Server-side, all base64url inputs are validated with this pattern; invalid encoding returns a 400:

$b64url_re = '/^[A-Za-z0-9\-_]+=*$/';

Security notes

  • Challenge expiry: sign-in challenges expire after 60 seconds – complete the flow promptly.
  • No user enumeration: sign-in endpoints return a generic “Invalid credentials” error regardless of whether the user or passkey exists.
  • HTTPS required: WebAuthn needs HTTPS in production (localhost is exempt for development).
  • Origin binding: the Relying Party ID must match the site’s domain.
  • Rate limiting: not built in – consider adding it on the sign-in endpoints to deter brute-force attempts.

Further reading

Close the CTA
Were you able to find what you were looking for?