antiwork/gumroad · error · VerificationError
unknown_credential
unknown_credential
Error message
unknown_credential
What it means
Raised in `Logins::PasskeysController#verified_credential` (logins/passkeys_controller.rb:47): `WebAuthn::Credential.from_get` parsed the assertion, but `WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)` found no stored credential with that WebAuthn id. The authenticator presented a key this server never registered (or no longer stores), so there is nothing to verify against — the controller returns the generic 422.
Source
Thrown at app/controllers/logins/passkeys_controller.rb:47
sign_in(user)
reset_two_factor_auth_login_session
merge_guest_cart_with_user_cart
refresh_passkey_setup_prompt(user)
Rails.logger.info("passkey.authentication.succeeded user_id=#{user.id} webauthn_credential_id=#{stored_credential.id}")
render json: { success: true, redirect_location: login_path_for(user) }
rescue VerificationError => e
log_authentication_failure(e.reason)
render json: { success: false, error_message: AUTHENTICATION_ERROR_MESSAGE }, status: :unprocessable_entity
end
private
def verified_credential(challenge)
map_webauthn_verification_errors do
webauthn_credential = WebAuthn::Credential.from_get(assertion_params)
stored_credential = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id)
raise VerificationError, "unknown_credential" if stored_credential.nil?
webauthn_credential.verify(
challenge,
public_key: stored_credential.public_key,
sign_count: stored_credential.sign_count,
user_verification: true
)
stored_credential.assign_attributes(sign_count: webauthn_credential.sign_count, last_used_at: Time.current)
stored_credential
end
end
def assertion_params
permitted_params = permitted_credential_params(response: [:authenticatorData, :clientDataJSON, :signature, :userHandle])
raise VerificationError, "malformed_credential" unless valid_assertion_params?(permitted_params)
permitted_paramsView on GitHub (pinned to afeacbd394)
Solutions
- If the passkey was deleted, re-register it: log in with password/2FA, then add a new passkey under Settings → Passkeys.
- Verify WebAuthn configuration (rp_id, origin) matches across environments — passkeys are bound to the relying party id.
- Clear the stale credential from the browser's saved passkeys (or the OS authenticator) so it stops being offered.
- Frontend: treat this 422 as "unknown credential" — offer account recovery or an alternative sign-in method instead of retry.
Example fix
# before — passkey removed server-side but still offered by the browser
user.webauthn_credentials.destroy(credential) # later: navigator.credentials.get offers it => unknown_credential
# after — when deleting server-side, also prevent resurface
def destroy
@webauthn_credential.destroy!
render json: { success: true }
end
# and in the login UI, fall back to password sign-in when the passkey POST 422s Defensive patterns
Strategy: try-catch
Type guard
const isRegisteredCredentialId = (id) => registeredCredentialIds.includes(id); // maintain client-side list from registration/last-known state
Try / catch
begin stored = WebauthnCredential.find_by_webauthn_id(webauthn_credential.id) raise VerificationError, "unknown_credential" if stored.nil? rescue VerificationError => e # generic 422; client should fall back to another sign-in method end
Prevention
- After deleting a passkey server-side, also remove it from the browser/OS credential manager when possible.
- Keep WebAuthn rp_id and origins identical across environments; passkeys are RP-bound.
- On 422, offer password/2FA sign-in and passkey re-registration rather than retry.
- During auth-failure spikes, grep logs for reason=unknown_credential to distinguish config drift from user error.
When it happens
Trigger: User removed the passkey in Settings → Passkeys from another device, then tries it here; the passkey was created for a different relying party (rp_id) so its id exists but not in this store; browser autofills a passkey belonging to another account/environment (staging vs production); the credential row was purged with account deletion.
Common situations: Deleted passkey still cached in the browser's credential manager; rp_id/rp_name config drift between environments (a passkey minted for staging rp used on production); users with multiple Gumroad accounts where the wrong one is offered.
Related errors
- We couldn't sign you in with that passkey. Please try again
- missing_challenge
- deleted_user
- Could not add this passkey. Please try again.
- Sorry, something went wrong. Please try again.
AI-assisted analysis of antiwork/gumroad@afeacbd394 (2026-08-21).
Data as JSON: /api/errors/b2390cd9716f5ea6.
Report an issue: GitHub.