Skip to content

Passkey Authentication

Overview

Passkey Authentication enables passwordless login to the Upvendo backoffice using the WebAuthn standard. Users can register biometric credentials (fingerprint, Face ID) or hardware security keys as passkeys, then use them for fast and secure authentication without typing a password. The system implements the full WebAuthn registration (attestation) and authentication (assertion) ceremony flows, with challenges stored per-user in MongoDB.

Passkeys are a modern, phishing-resistant alternative to traditional password-based authentication. They use public-key cryptography where the private key never leaves the user's device, making them immune to credential theft via phishing, keylogging, or database breaches.

Purpose

This page lets you manage passkey credentials for your account, including registering new passkeys with a custom name, viewing existing passkeys, and deleting passkeys you no longer use, as well as authenticating into the backoffice using a registered passkey instead of a password.

Key Concepts

  • Passkey: A WebAuthn credential (public key) stored on the user's device (or a hardware token) that enables passwordless authentication. Each passkey has a human-readable name, the serialized PublicKeyCredentialSource containing the public key and credential ID, a created_at timestamp (set at registration), and an updated_at timestamp that refreshes on each successful authentication.
  • WebAuthn Ceremony: The cryptographic handshake between the browser and server. Registration uses an attestation ceremony (creating a new credential via AuthenticatorAttestationResponseValidator), while login uses an assertion ceremony (proving possession of an existing credential via AuthenticatorAssertionResponseValidator). Both ceremonies involve a server-generated random 32-byte challenge stored as webauthn_challenge on the User document.
  • Relying Party (RP): The server entity that passkeys are bound to, defined by the app name (config('app.name')) and the hostname extracted from config('app.backoffice_url'). The RP ID is the domain where passkeys are registered and the only domain where they can be used for authentication.
  • Device Trust: During passkey authentication, users can optionally set trust_device: true in the PasskeyAuthRequest to mark the device as trusted. This flag is passed through the authentication flow and may affect session duration or future authentication requirements.
  • Challenge: A cryptographically random 32-byte value generated server-side using random_bytes(32) and stored on the user document as webauthn_challenge. Each challenge is single-use -- it ensures each ceremony is unique and prevents replay attacks. The challenge is overwritten when a new ceremony is initiated.

Route

  • Backoffice Route: /profile/passkeys (management), /login (authentication)
  • Backend Controller: app/Http/Controllers/Api/AuthController.php
  • Service: app/Services/PasskeyService.php
  • Auth Service: app/Services/AuthService.php (wraps passkey operations in auth flow)
  • Request Validators: app/Http/Requests/Auth/SetupPasskeyRequest.php, app/Http/Requests/Auth/PasskeyAuthRequest.php, app/Http/Requests/Auth/GetUserPasskeyRequest.php, app/Http/Requests/Auth/DeletePasskeyRequest.php
  • Vue Components: src/views/passkeys/PassKeys.vue, src/views/passkeys/PasskeySetupDialog.vue, src/views/passkeys/PasskeyDeleteDialog.vue, src/views/authentication/PasskeyAuthentication.vue, src/views/authentication/AuthMethodSelection.vue
  • Traits: app/Traits/DeviceTrackingTrait.php (provides IP detection, user agent parsing, device fingerprinting, and base64url encoding/decoding for WebAuthn)

Actions

Get Passkey Setup Options

Initiate the passkey registration ceremony by generating PublicKeyCredentialCreationOptions. The server creates a random 32-byte challenge, constructs the RP entity (using app name and backoffice hostname), constructs the user entity (using email, user ID, and display name), serializes the options to JSON, and stores them on the user's webauthn_challenge field. Returns the serialized options for the browser's navigator.credentials.create() API.

  • Endpoint: GET /api/auth/passkeys/setup (authenticated)
  • Response: Serialized PublicKeyCredentialCreationOptions JSON

Register a Passkey

Complete the passkey registration ceremony. Receives the attestation response from the browser along with a human-readable name for the passkey. The server:

  1. Deserializes the credential using the WebAuthn serializer
  2. Checks that no existing passkey has the same name (returns 400 if duplicate found)
  3. Validates that the response is an AuthenticatorAttestationResponse
  4. Creates a CeremonyStepManagerFactory (with localhost origins allowed in local environment)
  5. Validates the attestation response against the stored challenge and backoffice host
  6. Serializes the resulting PublicKeyCredentialSource and appends it to the user's passkeys array with name, passkey, and created_at fields
  7. Saves the updated passkeys array to MongoDB
  • Endpoint: POST /api/auth/passkeys/setup (authenticated)
  • Request Body: name (required, string, max 255), passkey (required, JSON)
  • Response: {status: "success", message: "Passkey registered successfully"}

Get Passkey Challenge

Initiate the passkey authentication ceremony for a given email address. The server looks up the user by email, retrieves all registered passkeys, converts them to PublicKeyCredentialDescriptor objects for the allowCredentials list, generates a random challenge, and stores the serialized PublicKeyCredentialRequestOptions on the user's webauthn_challenge field.

  • Endpoint: POST /api/auth/passkeys (guest route, no authentication required)
  • Request Body: email (required, string)
  • Response: Serialized PublicKeyCredentialRequestOptions JSON

Authenticate with Passkey

Complete the passkey authentication ceremony. The server:

  1. Deserializes the credential from the request
  2. Looks up the user by matching passkeys.passkey.publicKeyCredentialId against the credential ID
  3. Finds the specific passkey entry and deserializes its PublicKeyCredentialSource
  4. Validates the assertion response against the stored challenge, passkey source, and backoffice host
  5. Updates the passkey's serialized data (refreshing the counter) and sets updated_at
  6. Returns an authentication token
  • Endpoint: POST /api/auth/authenticate-passkey (guest route)
  • Request Body: passkey (required, JSON), trust_device (optional, boolean)
  • Response: Authentication token response

View Passkeys

List all registered passkeys for the authenticated user. Returns a sanitized list with only the id (same as name), name, and created_at timestamp (converted from MongoDB UTCDateTime to Unix timestamp). Sensitive cryptographic data is stripped from the response.

  • Endpoint: GET /api/auth/passkeys (authenticated)
  • Response: Array of {id, name, created_at} objects

Delete a Passkey

Remove a registered passkey by name. Filters the passkey out of the user's passkeys array using array_filter, re-indexes with array_values, and saves the updated array to MongoDB.

  • Endpoint: DELETE /api/auth/passkeys (authenticated)
  • Request Body: name (required, string)
  • Response: Success response

Fields

FieldIDTypeRequiredValidation
Passkey NamenameStringYes (setup/delete)required|string|max:255
Passkey CredentialpasskeyJSONYes (setup/auth)required|json -- serialized WebAuthn credential
Trust Devicetrust_deviceBooleanNo (auth only)boolean -- optional flag for device trust
EmailemailStringYes (challenge)required|string -- user email for lookup

Business Rules

  • A user cannot register two passkeys with the same name. The system iterates through existing passkeys and checks for name equality, returning a 400 error with message "Passkey with this id already exists" if a duplicate is found.
  • The WebAuthn RP ID is derived from the backoffice URL hostname (parse_url(config('app.backoffice_url'), PHP_URL_HOST)), so passkeys are domain-bound. They will not work if the backoffice domain changes -- users would need to re-register passkeys on the new domain.
  • In local development environments (app()->isLocal()), the CeremonyStepManagerFactory allows localhost as an allowed origin, enabling WebAuthn testing without HTTPS. In production, only the actual backoffice hostname is accepted.
  • Each successful passkey authentication updates the passkey's updated_at timestamp and refreshes the serialized PublicKeyCredentialSource with the latest signature counter value. This counter-increment mechanism detects cloned credentials.
  • The webauthn_challenge field on the user document is shared between registration and authentication ceremonies, meaning only one ceremony can be active at a time per user. Initiating a new ceremony overwrites the previous challenge.
  • Passkey user lookup during authentication is performed by querying the nested passkeys.passkey.publicKeyCredentialId field in MongoDB, with throw: false to return null instead of an exception if no user is found.
  • The NoneAttestationStatementSupport is the only attestation type registered, meaning the system accepts self-attestation from any authenticator without requiring a specific attestation certificate chain.

Customer Impact

  • Kiosk: Passkeys do not directly affect kiosk operations, as they are specific to backoffice authentication for merchant users.
  • Online Ordering: No direct impact on customer-facing online ordering. Passkeys are for merchant-side authentication only.
  • Backoffice UX: Passkeys provide a significantly faster and more secure login experience for merchants and staff. Users can authenticate with a fingerprint or face scan in seconds instead of remembering and typing passwords, reducing login friction and improving daily workflow efficiency.
  • Security Posture: Passkeys are immune to phishing attacks, password reuse vulnerabilities, and brute-force attempts, significantly improving the security of merchant accounts.

FAQs

What devices support passkeys?

Passkeys are supported on most modern devices and browsers that implement the WebAuthn standard, including Chrome 67+, Safari 14+, Firefox 60+, and Edge 18+. Biometric passkeys require a device with fingerprint or facial recognition hardware. Hardware security keys (e.g., YubiKey) are also supported as external authenticators.

Can I have multiple passkeys?

Yes. You can register multiple passkeys, each with a unique name. This is useful for having passkeys on different devices (e.g., "MacBook Pro", "iPhone 15", "Office YubiKey"). There is no hard limit on the number of passkeys per user.

What happens if I lose my device with the passkey?

You can still log in using your email and password or the OTP (one-time password) method. Then navigate to Profile > Passkeys to delete the lost device's passkey and register a new one on your current device. The lost passkey cannot be used by anyone else as it requires biometric verification on the device.

Are passkeys more secure than passwords?

Yes. Passkeys use public-key cryptography where the private key never leaves the device. They are resistant to phishing (the credential is domain-bound), credential stuffing (no password to reuse), brute-force attacks (no password to guess), and server-side breaches (only the public key is stored on the server).

Can I use passkeys across different Upvendo backoffice domains?

No. Passkeys are cryptographically bound to the specific domain (RP ID) they were registered on. If the backoffice URL changes (e.g., from app.example.com to new.example.com), existing passkeys will no longer work and must be re-registered on the new domain.

What authentication methods are available besides passkeys?

Upvendo supports three authentication methods: email/password login, OTP (one-time password) via email, and passkey authentication. Users can choose their preferred method on the login page via the AuthMethodSelection component.

How do I know which passkey was used for login?

When you view your passkeys via the management page, each passkey shows its created_at timestamp and the system tracks the updated_at timestamp which is refreshed on every successful authentication. The most recently updated passkey is the one that was last used to log in.

Can staff members use passkeys?

Yes. Any user account with access to the backoffice can register and use passkeys. This includes vendor owners, administrators, and staff members with appropriate permissions. Each user manages their own passkeys independently.

Troubleshooting

"Invalid passkey response" error during registration

This error occurs when the attestation ceremony validation fails. Common causes: the browser is not on the correct backoffice domain matching app.backoffice_url; the challenge has expired because the user took too long between getting setup options and completing registration; or the authenticator response is malformed. In development, verify that app()->isLocal() returns true so localhost origins are allowed.

Passkey login fails with "User not found"

The system looks up users by the credential's publicKeyCredentialId field in the nested passkeys.passkey.publicKeyCredentialId path. This error means no user has a passkey matching the presented credential. The passkey may have been deleted server-side, the user may be trying to authenticate on the wrong vendor/environment, or the credential may have been registered on a different Upvendo instance.

Registration prompt does not appear in the browser

Ensure the browser supports WebAuthn and that the page is served over HTTPS (required in production) or localhost (allowed in development). Some browsers require a secure context for the navigator.credentials.create() API. Also check that no browser extensions are blocking WebAuthn prompts, and that the user has not denied permission for the authenticator.

"Passkey with this id already exists" error

Each passkey must have a unique name per user. Choose a different, descriptive name for the new passkey (e.g., "Work Laptop" instead of just "Laptop"), or delete the existing passkey with that name first via the Profile > Passkeys management page.

Passkey works on one browser but not another

Passkey credentials are synced within platform ecosystems (e.g., Apple iCloud Keychain syncs across Safari on Mac/iPhone, Google Password Manager syncs across Chrome devices) but are typically not available across different ecosystems. Register separate passkeys for each browser/device ecosystem you use, giving each a descriptive name.

Passkey authentication works locally but fails in production

Check that app.backoffice_url is correctly set to the production domain. The RP ID used during registration must match the RP ID used during authentication. If the production URL was changed after passkeys were registered, users must re-register their passkeys.

Technical Details

WebAuthn Serialization

The PasskeyService constructor initializes a custom WebauthnSerializerFactory with NoneAttestationStatementSupport as the only registered attestation type. This serializer is used throughout the service to convert between PHP objects and JSON for both storage and API communication. The AttestationStatementSupportManager only supports none attestation, meaning the system accepts self-attestation from any authenticator.

Credential Storage Format

Each passkey entry in the user's passkeys array has the following structure:

  • name: Human-readable identifier (string)
  • passkey: Serialized PublicKeyCredentialSource (object with publicKeyCredentialId, public key data, counter, etc.)
  • created_at: MongoDB UTCDateTime set at registration
  • updated_at: MongoDB UTCDateTime refreshed on each successful authentication

User Lookup Strategy

During authentication, the service queries MongoDB using the nested field path passkeys.passkey.publicKeyCredentialId to find the user by credential ID. This avoids requiring the user to provide their email during the authentication step -- the credential itself identifies the user. The retrieveByFilter method is called with throw: false to handle the case where no matching user exists gracefully.

Device Tracking Integration

The DeviceTrackingTrait used by PasskeyService provides several WebAuthn-supporting utilities: base64url_decode and base64url_encode for the URL-safe base64 encoding required by WebAuthn, generateChallengeToken for creating random tokens, and getClientIp/getUserAgent/generateDeviceFingerprint for device identification when the trust_device flag is used.

Localization

The passkey feature includes translations for both English and Dutch via the src/plugins/i18n/locales/modules/en/passkeys.ts and src/plugins/i18n/locales/modules/nl/passkeys.ts locale files. These provide translated strings for the setup dialog, delete confirmation, passkey list, and authentication flow UI elements.

Backoffice Components

The passkey management interface consists of several Vue components working together:

  • PassKeys.vue: Main passkey list page at /profile/passkeys, showing all registered passkeys with name, creation date, and delete action
  • PasskeySetupDialog.vue: Modal dialog for registering a new passkey, prompting for a name and triggering the WebAuthn browser prompt
  • PasskeyDeleteDialog.vue: Confirmation dialog before deleting a passkey
  • PasskeyAuthentication.vue: Login page component that handles the passkey assertion ceremony
  • AuthMethodSelection.vue: Login page component allowing users to switch between password, OTP, and passkey authentication methods

Assistant Guidance

When users ask about passkey setup, walk them through the two-step registration flow: first request the setup options (which triggers the browser's authenticator prompt), then confirm with biometrics or a security key. Emphasize that passkey names must be unique and suggest descriptive names like "MacBook Pro", "iPhone 15", or "Office YubiKey". If users report authentication issues, check whether the backoffice domain matches the RP ID used during registration. Remind users that passkeys are an alternative to password login, not a replacement for the account itself -- they can always fall back to email/password or OTP authentication. For security-conscious users, explain that passkeys are phishing-resistant because they are cryptographically bound to the domain. If a user asks about device trust, explain the optional trust_device flag in the authentication request.

Relations

Depends On

  • Authentication System: Passkeys are an alternative authentication method alongside email/password and OTP, integrated via AuthService.
  • User Model: Passkeys are stored in the passkeys array field and webauthn_challenge field on the User document in MongoDB.
  • WebAuthn Library: Uses the web-auth/webauthn-lib PHP package for ceremony validation, credential serialization/deserialization, and attestation statement support.
  • Device Tracking Trait: Provides utilities for IP detection, user agent parsing, device fingerprinting, and base64url encoding/decoding used in the WebAuthn flow.

Affects

  • Login Flow: Adds passkey as a third authentication option on the login page alongside email/password and OTP, with a dedicated AuthMethodSelection component.
  • User Profile: Adds a Passkeys management section (/profile/passkeys) to the user profile with setup, list, and delete functionality.
  • Security: Improves overall account security by enabling phishing-resistant, passwordless authentication for merchant and staff accounts.
  • Session Management: Passkey authentication with device trust may influence session token duration and re-authentication requirements.