By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future | HackerNoon
Computing

Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future | HackerNoon

News Room
Last updated: 2026/03/13 at 12:36 PM
News Room Published 13 March 2026
Share
Passkeys in Symfony 7.4: How to Build a Completely Passwordless Future | HackerNoon
SHARE

In the modern web era, passwords are no longer sufficient. They are the root cause of over 80% of data breaches, subject to phishing, reuse, and terrible complexity rules. The industry has spoken: Passkeys are the future.

Passkeys, built on the Web Authentication (WebAuthn) and FIDO2 standards, replace traditional passwords with cryptographic key pairs. Your device (iPhone, Android, Windows Hello, YubiKey) stores a private key, while the server only ever sees the public key. No hashes to steal, no passwords to reset, and inherently phishing-resistant.

In this comprehensive guide, we will build a 100% passwordless authentication system using Symfony and the official web-auth/webauthn-symfony-bundle. We will eliminate the concept of a password entirely from our application. No fallback, no “reset password” links. Just pure, secure, biometric-backed passkeys.

Core Architecture & Requirements

Passkeys work by replacing a shared secret (password) with a public/private key pair. The private key never leaves the user’s Apple device (iPhone, Mac, iPad), and the public key is stored on your Symfony server.

Technical Stack

  • PHP: 8.2 or higher (Required for the latest WebAuthn libs)
  • Symfony: 7.4 LTS
  • Database: PostgreSQL, MySQL, or SQLite for dev (to store Credential Sources)
  • Primary Library: web-auth/webauthn-symfony-bundle

Essential Packages

Run the following command to install the necessary dependencies:

composer require web-auth/webauthn-symfony-bundle:^5.2 
                 web-auth/webauthn-stimulus:^5.2 
                 symfony/uid:^7.4

We use @simplewebauthn/browser via AssetMapper (which provides excellent wrapper functions for the native browser WebAuthn APIs) because Apple Passkeys require a frontend interaction that is best handled via a Stimulus controller in a modern Symfony environment, or you can use React/Vue modules.

Database Schema: The Credential Source

This is where our application dramatically diverges from a traditional Symfony app. We are going to strip passwords entirely from the system.

Standard Symfony User entities aren’t equipped to store Passkey metadata (like AAGUIDs or public key Cose algorithms). We need a dedicated entity to store the credentials.

The User Entity

Our User entity implements SymfonyComponentSecurityCoreUserUserInterface. Noticeably absent is the PasswordAuthenticatedUserInterface.

namespace AppEntity;

use AppRepositoryUserRepository;
use DoctrineORMMapping as ORM;
use SymfonyComponentSecurityCoreUserUserInterface;
use SymfonyComponentUidUuid;
use SymfonyComponentValidatorConstraints as Assert;

#[ORMEntity(repositoryClass: UserRepository::class)]
#[ORMTable(name: '`user`')]
class User implements UserInterface
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn]
    private ?int $id = null;

    #[ORMColumn(length: 255, unique: true)]
    private ?string $userHandle = null;

    #[ORMColumn(length: 180, unique: true)]
    #[AssertNotBlank]
    #[AssertEmail]
    private ?string $email = null;

    public function __construct()
    {
        $this->userHandle = Uuid::v4()->toRfc4122();
    }

    ...
}

The PublicKeyCredentialSource Entity

A single user can have multiple passkeys (e.g., Face ID on their phone, Touch ID on their Mac, a YubiKey on their keychain). We need an entity to store these public keys and their associated metadata.

Create src/Entity/PublicKeyCredentialSource.php. This entity must be capable of translating to and from the bundle’s native WebauthnPublicKeyCredentialSource object.

Crucially, we must preserve the TrustPath. Failing to do so destroys the attestation data needed if you ever require high-security enterprise hardware keys.

namespace AppEntity;

use AppRepositoryPublicKeyCredentialSourceRepository;
use DoctrineORMMapping as ORM;
use WebauthnPublicKeyCredentialSource as WebauthnSource;

#[ORMEntity(repositoryClass: PublicKeyCredentialSourceRepository::class)]
#[ORMTable(name: 'webauthn_credentials')]
class PublicKeyCredentialSource extends WebauthnSource
{
    #[ORMId]
    #[ORMGeneratedValue]
    #[ORMColumn]
    private ?int $id = null;

    public function getId(): ?int
    {
        return $this->id;
    }
}

The CredentialSourceRepository

You must also implement a CredentialSourceRepository that implements WebauthnBundleRepositoryPublicKeyCredentialSourceRepository.

namespace AppRepository;

use AppEntityPublicKeyCredentialSource;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;
use SymfonyComponentObjectMapperObjectMapperInterface;
use WebauthnBundleRepositoryPublicKeyCredentialSourceRepositoryInterface;
use WebauthnBundleRepositoryCanSaveCredentialSource;
use WebauthnPublicKeyCredentialSource as WebauthnSource;
use WebauthnPublicKeyCredentialUserEntity;

class PublicKeyCredentialSourceRepository extends ServiceEntityRepository implements PublicKeyCredentialSourceRepositoryInterface, CanSaveCredentialSource
{
    public function __construct(ManagerRegistry $registry, private readonly ObjectMapperInterface $objectMapper)
    {
        parent::__construct($registry, PublicKeyCredentialSource::class);
    }

    public function findOneByCredentialId(string $publicKeyCredentialId): ?WebauthnSource
    {
        return $this->findOneBy(['publicKeyCredentialId' => $publicKeyCredentialId]);
    }

    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
    {
        return $this->findBy(['userHandle' => $publicKeyCredentialUserEntity->id]);
    }

    public function saveCredentialSource(WebauthnSource $publicKeyCredentialSource): void
    {
        $entity = $this->findOneBy(['publicKeyCredentialId' => base64_encode($publicKeyCredentialSource->publicKeyCredentialId)])
            ?? $this->objectMapper->map($publicKeyCredentialSource, PublicKeyCredentialSource::class);

        $this->getEntityManager()->persist($entity);
        $this->getEntityManager()->flush();
    }
}

The WebAuthn bundle relies on abstract interfaces to find and persist users and credentials. Our repositories must implement these interfaces.

The UserRepository

The UserRepository implements PublicKeyCredentialUserEntityRepositoryInterface. Because we want the bundle to handle user creation automatically during a passkey registration, we also implement CanRegisterUserEntity and CanGenerateUserEntity.

namespace AppRepository;

use AppEntityUser;
use DoctrineBundleDoctrineBundleRepositoryServiceEntityRepository;
use DoctrinePersistenceManagerRegistry;
use SymfonyComponentUidUuid;
use WebauthnBundleRepositoryCanGenerateUserEntity;
use WebauthnBundleRepositoryCanRegisterUserEntity;
use WebauthnBundleRepositoryPublicKeyCredentialUserEntityRepositoryInterface;
use WebauthnExceptionInvalidDataException;
use WebauthnPublicKeyCredentialUserEntity;

class UserRepository extends ServiceEntityRepository implements PublicKeyCredentialUserEntityRepositoryInterface, CanRegisterUserEntity, CanGenerateUserEntity
{
    public function __construct(ManagerRegistry $registry)
    {
        parent::__construct($registry, User::class);
    }

    public function saveUserEntity(PublicKeyCredentialUserEntity $userEntity): void
    {
        $user = new User();
        $user->setEmail($userEntity->name);
        $user->setUserHandle($userEntity->id);

        $this->getEntityManager()->persist($user);
        $this->getEntityManager()->flush();
    }

    public function generateUserEntity(?string $username, ?string $displayName): PublicKeyCredentialUserEntity
    {
        return new PublicKeyCredentialUserEntity(
            $username ?? '',
            Uuid::v4()->toRfc4122(),
            $displayName ?? $username ?? ''
        );
    }

    ...

Configuration: Bridging Symfony and Apple

Apple requires specific “Relying Party” (RP) information. This identifies your application to the user’s iCloud Keychain.

WebAuthn Configuration

Create or update config/packages/webauthn.yaml:

webauthn:
    allowed_origins: ['%env(WEBAUTHN_ALLOWED_ORIGINS)%']
    credential_repository: 'AppRepositoryPublicKeyCredentialSourceRepository'
    user_repository: 'AppRepositoryUserRepository'
    creation_profiles:
        default:
            rp:
                name: '%env(RELYING_PARTY_NAME)%'
                id: '%env(RELYING_PARTY_ID)%'
    request_profiles:
        default:
            rp_id: '%env(RELYING_PARTY_ID)%'

WebAuthn is incredibly strict about domains. A passkey created for example.com cannot be used on phishing-example.com. To ensure our application is portable across environments, we define our Relying Party (RP) settings in the .env file.

Open .env or .env.local and add:

###> web-auth/webauthn-symfony-bundle ###
RELYING_PARTY_ID=localhost
RELYING_PARTY_NAME="My Application"
WEBAUTHN_ALLOWED_ORIGINS=localhost
###< web-auth/webauthn-symfony-bundle ###

In production, RELYINGPARTYID must be your exact root domain (e.g., example.com), and WebAuthn requires a secure HTTPS context. Browsers only exempt localhost for development.

The Registration Flow (Creation)

Passkey registration is a two-step handshake:

  1. Challenge: The server generates a unique challenge and “Creation Options.”
  2. Attestation: The browser (Safari/Chrome) asks the user for FaceID/TouchID, signs the challenge, and sends the “Attestation Object” back to the server.

The Frontend: Stimulus and CSRF

Security is paramount. Even though WebAuthn is inherently phishing-resistant, your endpoints are still vulnerable to traditional Cross-Site Request Forgery (CSRF) if left unprotected. We will pass Symfony’s built-in CSRF tokens via headers in our fetch() calls.

Assuming you have a standard CSRF helper (like csrfprotectioncontroller.js that extracts the token from a meta tag or hidden input), we inject it into our Passkey controller.

import { Controller } from '@hotwired/stimulus';
import { startRegistration, startAuthentication } from '@simplewebauthn/browser';
import { generateCsrfHeaders } from './csrf_protection_controller.js';

export default class extends Controller {
    static values = {
        optionsUrl: String,
        resultUrl: String,
        isLogin: Boolean
    }

    connect() {
        console.log('Passkey controller connected! 🔑');
    }

    async submit(event) {
        event.preventDefault();

        const username = this.element.querySelector('[name="username"]')?.value;

        if (!this.isLoginValue && !username) {
            alert('Please provide a username/email');
            return;
        }

        const csrfHeaders = generateCsrfHeaders(this.element);

        try {
            // 1. Fetch options
            const response = await fetch(this.optionsUrlValue, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...csrfHeaders },
                body: username ? JSON.stringify({ username: username, displayName: username }) : '{}'
            });

            if (!response.ok) {
                const errorData = await response.json().catch(() => ({}));
                throw new Error(errorData.errorMessage || 'Failed to fetch WebAuthn options from server');
            }

            const options = await response.json();

            // 2. Trigger Apple's Passkey UI (Create or Get)
            let credential;
            if (this.isLoginValue) {
                credential = await startAuthentication({ optionsJSON: options });
            } else {
                credential = await startRegistration({ optionsJSON: options });
            }

            // 3. Send result back to verify
            const result = await fetch(this.resultUrlValue, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json', ...csrfHeaders },
                body: JSON.stringify(credential)
            });

            if (result.ok) {
                window.location.reload();
            } else {
                const errorText = await result.text();
                alert('Authentication failed: ' + errorText);
            }
        } catch (e) {
            console.error(e);
            alert('WebAuthn process failed: ' + e.message);
        }
    }
}

Routing

You need to ensure the routing type for webauthn exists. Create config/routes/webauthn_routes.yaml:

webauthn_routes:
    resource: .
    type: webauthn

Security Bundle Integration

To allow users to log in with their Passkey, we need to configure the Symfony Guard (now the Authenticator system).

In config/packages/security.yaml:

security:
    providers:
        app_user_provider:
            entity:
                class: AppEntityUser
                property: email
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            lazy: true
            provider: app_user_provider

            webauthn:
                authentication:
                    routes:
                        options_path: /login/passkey/options
                        result_path: /login/passkey/result
                registration:
                    enabled: true
                    routes:
                        options_path: /register/passkey/options
                        result_path: /register/passkey/result
                success_handler: AppSecurityAuthenticationSuccessHandler
                failure_handler: AppSecurityAuthenticationFailureHandler

            logout:
                path: app_logout
    access_control:
        - { path: ^/dashboard, roles: ROLE_USER }

The Authentication Failure Handler

Because WebAuthn ceremonies involve AJAX fetch() requests from the frontend, a standard Symfony redirect on failure (e.g., trying to register an email that already exists) will be silently swallowed by the browser, resulting in a frustrating user experience.

We implement a custom AuthenticationFailureHandler that returns a clean 401 Unauthorized JSON response when the request is AJAX.

Create src/Security/AuthenticationFailureHandler.php:

namespace AppSecurity;

use SymfonyComponentHttpFoundationJsonResponse;
use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentHttpFoundationResponse;
use SymfonyComponentRoutingGeneratorUrlGeneratorInterface;
use SymfonyComponentSecurityCoreExceptionAuthenticationException;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationFailureHandlerInterface;
use SymfonyComponentSecurityHttpSecurityRequestAttributes;

readonly class AuthenticationFailureHandler implements AuthenticationFailureHandlerInterface
{
    public function __construct(private UrlGeneratorInterface $urlGenerator) {}

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): RedirectResponse|JsonResponse
    {
        if ($request->getContentTypeFormat() === 'json' || $request->isXmlHttpRequest()) {
            return new JsonResponse([
                'status' => 'error',
                'errorMessage' => $exception->getMessageKey(),
            ], Response::HTTP_UNAUTHORIZED);
        }

        // Store the error in the session
        $request->getSession()->set(SecurityRequestAttributes::AUTHENTICATION_ERROR, $exception);

        return new RedirectResponse($this->urlGenerator->generate('app_login'));
    }
}

The Authentication Success Handler

Since Passkeys often bypass the traditional login form, you need to define where the user goes after a successful “Handshake.”

namespace AppSecurity;

use SymfonyComponentHttpFoundationRedirectResponse;
use SymfonyComponentHttpFoundationRequest;
use SymfonyComponentRoutingGeneratorUrlGeneratorInterface;
use SymfonyComponentSecurityCoreAuthenticationTokenTokenInterface;
use SymfonyComponentSecurityHttpAuthenticationAuthenticationSuccessHandlerInterface;

readonly class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function __construct(private UrlGeneratorInterface $urlGenerator) {}

    public function onAuthenticationSuccess(Request $request, TokenInterface $token): RedirectResponse
    {
        return new RedirectResponse($this->urlGenerator->generate('app_dashboard'));
    }
}

Verification & Apple-Specific Gotchas

  1. HTTPS is mandatory: Browsers will not expose navigator.credentials on insecure origins (except localhost).
  2. RP ID Match: Ensure the id in webauthn.yaml exactly matches your domain. If you are on dev.example.com, your RP ID should be example.com.
  3. Apple AAGUID: Apple devices often return a “Zero AAGUID” (all zeros). If your library is configured to strictly validate authenticators via metadata, you may need to allow “Unknown Authenticators” in your configuration.

Conclusion

Transitioning to Apple Passkeys with Symfony 7.4 isn’t just a security upgrade; it’s a significant improvement to your user experience. By removing the friction of password managers, “forgot password” emails, and complex character requirements, you increase conversion and user retention.

As a senior developer or lead, your priority is ensuring that this implementation remains maintainable. By sticking to the WebAuthn-Symfony-Bundle and PHP 8.x attributes, you ensure that your codebase remains idiomatic and ready for future Symfony LTS releases.

Summary Checklist for Deployment

  • SSL/TLS: Ensure your production environment uses a valid certificate (Passkeys will fail on plain HTTP).
  • RP ID Strategy: Decide if you want to support subdomains by setting your Relaying Party ID to the top-level domain.
  • Backup Methods: Always provide a secondary login method (like Magic Links or a traditional password) for users on older devices that do not support FIDO2.
  • Metadata Validation: For high-security apps, consider enabling the web-auth/webauthn-metadata-service to verify that the Passkey is indeed coming from an Apple device and not an unauthorized emulator.

Source Code: You can find the full implementation and follow the project’s progress on GitHub: [https://github.com/mattleads/PasskeysAuth]

Let’s Connect!

If you found this helpful or have questions about the implementation, I’d love to hear from you. Let’s stay in touch and keep the conversation going across these platforms:

  • LinkedIn: [https://www.linkedin.com/in/matthew-mochalkin/]
  • X (Twitter): [https://x.com/MattLeads]
  • Telegram: [https://t.me/MattLeads]
  • GitHub: [https://github.com/mattleads]

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Beyond the Code: Hiring for Cultural Alignment Beyond the Code: Hiring for Cultural Alignment
Next Article AI advised someone to stick garlic where the sun don’t shine AI advised someone to stick garlic where the sun don’t shine
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Report: TerraPower leadership faces tough questions about Gates, Myhrvold and Epstein ties
Report: TerraPower leadership faces tough questions about Gates, Myhrvold and Epstein ties
Computing
One UI 9 could make Samsung Browser better for juggling multiple pages
One UI 9 could make Samsung Browser better for juggling multiple pages
News
Ninja’s star Creami ice cream maker is on sale for 9
Ninja’s star Creami ice cream maker is on sale for $169
News
Panther Lake Tuning For The Intel Idle Driver In Linux 7.1
Panther Lake Tuning For The Intel Idle Driver In Linux 7.1
Computing

You Might also Like

Report: TerraPower leadership faces tough questions about Gates, Myhrvold and Epstein ties
Computing

Report: TerraPower leadership faces tough questions about Gates, Myhrvold and Epstein ties

4 Min Read
Panther Lake Tuning For The Intel Idle Driver In Linux 7.1
Computing

Panther Lake Tuning For The Intel Idle Driver In Linux 7.1

2 Min Read
Xiaomi unveils 15 Ultra phone as its 15th anniversary approaches · TechNode
Computing

Xiaomi unveils 15 Ultra phone as its 15th anniversary approaches · TechNode

3 Min Read
7 Iconic 20th-Century Ad Campaigns and What Today’s Marketers Can Learn From Them | HackerNoon
Computing

7 Iconic 20th-Century Ad Campaigns and What Today’s Marketers Can Learn From Them | HackerNoon

16 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?