Appearance
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 serializedPublicKeyCredentialSourcecontaining the public key and credential ID, acreated_attimestamp (set at registration), and anupdated_attimestamp 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 viaAuthenticatorAssertionResponseValidator). Both ceremonies involve a server-generated random 32-byte challenge stored aswebauthn_challengeon 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 fromconfig('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: truein thePasskeyAuthRequestto 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 aswebauthn_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
PublicKeyCredentialCreationOptionsJSON
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:
- Deserializes the credential using the WebAuthn serializer
- Checks that no existing passkey has the same name (returns 400 if duplicate found)
- Validates that the response is an
AuthenticatorAttestationResponse - Creates a
CeremonyStepManagerFactory(with localhost origins allowed in local environment) - Validates the attestation response against the stored challenge and backoffice host
- Serializes the resulting
PublicKeyCredentialSourceand appends it to the user'spasskeysarray withname,passkey, andcreated_atfields - 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
PublicKeyCredentialRequestOptionsJSON
Authenticate with Passkey
Complete the passkey authentication ceremony. The server:
- Deserializes the credential from the request
- Looks up the user by matching
passkeys.passkey.publicKeyCredentialIdagainst the credential ID - Finds the specific passkey entry and deserializes its
PublicKeyCredentialSource - Validates the assertion response against the stored challenge, passkey source, and backoffice host
- Updates the passkey's serialized data (refreshing the counter) and sets
updated_at - 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
| Field | ID | Type | Required | Validation |
|---|---|---|---|---|
| Passkey Name | name | String | Yes (setup/delete) | required|string|max:255 |
| Passkey Credential | passkey | JSON | Yes (setup/auth) | required|json -- serialized WebAuthn credential |
| Trust Device | trust_device | Boolean | No (auth only) | boolean -- optional flag for device trust |
email | String | Yes (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()), theCeremonyStepManagerFactoryallowslocalhostas 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_attimestamp and refreshes the serializedPublicKeyCredentialSourcewith the latest signature counter value. This counter-increment mechanism detects cloned credentials. - The
webauthn_challengefield 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.publicKeyCredentialIdfield in MongoDB, withthrow: falseto return null instead of an exception if no user is found. - The
NoneAttestationStatementSupportis 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: SerializedPublicKeyCredentialSource(object withpublicKeyCredentialId, public key data, counter, etc.)created_at: MongoDB UTCDateTime set at registrationupdated_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 actionPasskeySetupDialog.vue: Modal dialog for registering a new passkey, prompting for a name and triggering the WebAuthn browser promptPasskeyDeleteDialog.vue: Confirmation dialog before deleting a passkeyPasskeyAuthentication.vue: Login page component that handles the passkey assertion ceremonyAuthMethodSelection.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
passkeysarray field andwebauthn_challengefield on theUserdocument in MongoDB. - WebAuthn Library: Uses the
web-auth/webauthn-libPHP 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
AuthMethodSelectioncomponent. - 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.