Skip to content

WebAuthn Passwordless Authentication

Doggy implements WebAuthn standard authentication via web-auth/webauthn-symfony-bundle, supporting fingerprint, face ID, and security keys.

Overview

WebAuthn credentials are stored in src/Entity/Security/WebauthnCredential.php (table webauthn_credentials), linked to App\Entity\Organization\Employee (the user entity).

The credential repository implements PublicKeyCredentialSourceRepositoryInterface for full credential lifecycle management.

Configuration

yaml
# config/packages/webauthn.yaml
webauthn:
  credential_repository: 'App\Repository\Security\WebauthnCredentialRepository'
  user_repository: 'App\Repository\Organization\EmployeeRepository'
  creation_profiles:
    default:
      rp:
        name: '%env(RELYING_PARTY_NAME)%'
        id: '%env(RELYING_PARTY_ID)%'
      public_key_credential_parameters:
        - -7    # ES256
        - -257  # RS256
      authenticator_selection_criteria:
        resident_key: required
        user_verification: required
  request_profiles:
    default:
      rp_id: '%env(RELYING_PARTY_ID)%'
      user_verification: required
  allowed_origins: ['http://localhost:8000']

Security Firewall

yaml
# config/packages/security.yaml
security:
  firewalls:
    main:
      custom_authenticator: App\Security\AppCustomAuthenticator
      webauthn:
        success_handler: App\Security\WebauthnSuccessHandler
        authentication:
          enabled: true
          routes:
            options_path: '/login/webauthn/options'
            result_path: '/login/webauthn/result'

Credential Entity

php
// src/Entity/Security/WebauthnCredential.php
class WebauthnCredential
{
    private string $publicKeyCredentialId;
    private string $type;          // 'public-key'
    private array $transports;     // ['usb', 'nfc', 'internal']
    private string $attestationType;
    private string $credentialPublicKey;
    private string $userHandle;    // Employee ID
    private int $counter;
    private ?string $deviceName;   // e.g. "Chrome on Mac"

    #[ORM\ManyToOne(targetEntity: Employee::class, inversedBy: 'passkeys')]
    private ?Employee $employee = null;
}

Login Flow

Register Passkey

  1. User logs in and navigates to security settings
  2. Request options from /user/webauthn/register/options
  3. Browser creates credential via WebAuthn API
  4. Result submitted to /user/webauthn/register/result
  5. Public key stored in webauthn_credentials table

Passwordless Login

  1. Enter username, click "Passkey Login"
  2. POST to /login/webauthn/options for challenge
  3. Browser signs via WebAuthn API
  4. Result submitted to /login/webauthn/result
  5. WebauthnSuccessHandler processes success (updates device, timestamp)

Success Handler

App\Security\WebauthnSuccessHandler:

  1. Updates credential last-used timestamp
  2. Auto-identifies device name (User-Agent parsing)
  3. Checks first-login status, redirects to password setup
  4. Supports password reset flow

Credential Repository

App\Repository\Security\WebauthnCredentialRepository implements:

  • findOneByCredentialId(): Find credential by ID
  • findAllForUserEntity(): Get all credentials for a user
  • saveCredentialSource(): Create/update credential
  • parseUserAgent(): Device name recognition

Open Source under MIT | Copyright © 2026 Doggy