Replacing passwords sounds simple until you already have thousands of users who depend on them. A production website cannot safely remove password login overnight, tell everyone to create a passkey, and hope account recovery still works.
A better approach is gradual migration. Keep the existing PHP login system working, allow signed-in users to register one or more passkeys, offer passkeys beside passwords on the login screen, and only consider reducing password dependence after users have reliable recovery options.
Direct Answer
You can add passkeys to an existing PHP login without removing passwords by treating WebAuthn credentials as an additional authentication method linked to the same user account. Keep password login available during migration, register passkeys only after securely identifying the user, verify every WebAuthn challenge on the server, and use passkey autofill so users do not have to choose between separate login pages.
What a Passkey Changes in Your Authentication System
Traditional password authentication depends on a shared secret. The user knows a password and your server stores a protected representation of that password.
Passkeys use a different model. WebAuthn creates a public and private key pair for the account. The private key stays with the user's authenticator, while your server stores the public credential information required to verify future authentication attempts.
During login, your server generates a unique challenge. The authenticator signs authentication data associated with that challenge, and your server verifies the response.
This matters because the user is no longer typing a reusable authentication secret into your website. Passkeys are also scoped to the relying party, which gives them strong resistance to traditional credential phishing.
What Does Not Change
Adding passkeys does not mean rewriting your entire application.
Your existing systems can continue handling:
- User IDs
- Profiles
- Authorization roles
- PHP sessions
- Shopping carts
- Permissions
- Account settings
Passkeys change how you prove the user is allowed to start an authenticated session. They do not need to replace the application's authorization model.
The Migration Architecture
Instead of thinking of an account as having one password, think of it as supporting multiple authentication methods.
User Account
|
|-- Password
|
|-- Passkey 1
|
|-- Passkey 2
|
|-- Recovery Method
|
|-- Active Sessions
This structure is useful because one person may have multiple passkeys. A user might have a passkey available through a phone, another credential through a password manager, and perhaps a hardware security key.
Your database should therefore not add a single passkey column to the users table.
Use a one-to-many relationship instead.
Example Database Structure
users
-----
id
email
password_hash
created_at
webauthn_credentials
--------------------
id
user_id
credential_id
public_key
sign_count
transports
created_at
last_used_at
label
The exact fields depend on the WebAuthn server library you use, but the important design decision is that one account can own multiple credentials.
Step 1: Keep Your Existing Password Login
The safest migration begins without changing the existing login path.
If your current system accepts an email and password, continue supporting it while passkeys are introduced.
<form method="post" action="/login">
<label for="email">Email</label>
<input
id="email"
name="email"
type="email"
autocomplete="username webauthn"
required
>
<label for="password">Password</label>
<input
id="password"
name="password"
type="password"
autocomplete="current-password"
>
<button type="submit">
Sign in
</button>
</form>
The important detail is:
autocomplete="username webauthn"
Supporting browsers can use this field to present passkeys through the normal credential autofill experience.
This is much better than making users remember whether they previously enabled a password, passkey, or both.
Step 2: Register Passkeys From an Authenticated Account
One of the easiest migration mistakes is allowing a person to attach a new passkey to an account before you have securely established which account they control.
A straightforward first deployment is to offer passkey registration inside account settings after the user has already authenticated.
For example:
Account Settings
Security
Password
Enabled
Passkeys
No passkeys added
[ Add a passkey ]
When the user clicks the button, the browser should not invent registration parameters itself.
Your server creates the registration options, including the challenge and relying-party information.
PHP Registration Endpoint Concept
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit;
}
$userId = $_SESSION['user_id'];
/*
* Use a maintained WebAuthn server library here.
*
* The library should generate:
* - challenge
* - RP information
* - user identifier
* - supported algorithms
* - existing credential exclusions
*/
$options = generate_webauthn_registration_options(
user_id: $userId
);
/*
* Save the challenge in server-side session state.
*/
$_SESSION['webauthn_registration_challenge']
= $options['challenge'];
header('Content-Type: application/json');
echo json_encode($options);
This example intentionally leaves cryptographic WebAuthn operations to a server library. Implementing signature parsing, authenticator data validation, CBOR processing, and credential verification manually is an unnecessary security risk for most PHP projects.
Step 3: Create the Passkey in the Browser
After receiving creation options from your backend, the browser calls WebAuthn.
The core browser operation is:
const credential = await navigator.credentials.create({
publicKey: publicKeyOptions
});
Modern applications also need to safely convert binary WebAuthn values when transferring them through JSON.
Your production implementation should use the conversion helpers supported by your browser target or the client utilities provided by the WebAuthn library you selected.
After creation succeeds, send the credential response to your PHP verification endpoint.
const response = await fetch("/api/passkeys/register/verify", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify(
serializeCredential(credential)
)
});
if (!response.ok) {
throw new Error("Passkey registration failed");
}
Creating a credential in the browser does not mean registration is finished.
The server must verify it before saving anything.
Step 4: Verify Registration on the Server
This is the security boundary developers should pay the most attention to.
Your server-side WebAuthn verification process needs to check the data returned by the authenticator against values your application expects.
Important verification includes:
- The expected challenge
- The expected website origin
- The expected relying-party ID
- The credential's cryptographic response
- Required user-presence or verification conditions
- Whether the credential is already registered
- Whether the cryptographic algorithm is one your application accepts
Never accept a credential simply because the browser returned a JSON object containing a credential ID.
Conceptual PHP Verification
<?php
session_start();
if (!isset($_SESSION['user_id'])) {
http_response_code(401);
exit;
}
$payload = json_decode(
file_get_contents('php://input'),
true
);
$expectedChallenge =
$_SESSION['webauthn_registration_challenge']
?? null;
if (!$expectedChallenge) {
http_response_code(400);
exit('Missing challenge');
}
/*
* Verification belongs in a trusted WebAuthn library.
*/
$result = verify_webauthn_registration(
credential: $payload,
expected_challenge: $expectedChallenge,
expected_origin: 'https://example.com',
expected_rp_id: 'example.com'
);
unset(
$_SESSION['webauthn_registration_challenge']
);
if (!$result->verified) {
http_response_code(400);
exit('Invalid WebAuthn registration');
}
/*
* Save the verified credential and public key.
*/
save_passkey(
user_id: $_SESSION['user_id'],
credential: $result
);
echo json_encode([
'success' => true
]);
The challenge should be short-lived and single-use. Authentication challenges exist to prevent replay of an old valid response.
Step 5: Add Passkey Login Without Removing Password Login
Once some users have registered passkeys, your login page needs to support both groups:
- Users who only have passwords
- Users who have both passwords and passkeys
- Users who eventually become passkey-first
Conditional WebAuthn mediation is useful here because it integrates passkeys with browser credential autofill.
async function startPasskeyAutofill() {
if (!window.PublicKeyCredential) {
return;
}
const optionsResponse =
await fetch("/api/passkeys/login/options");
if (!optionsResponse.ok) {
return;
}
const publicKey =
await optionsResponse.json();
try {
const credential =
await navigator.credentials.get({
publicKey:
decodeAuthenticationOptions(publicKey),
mediation: "conditional"
});
if (!credential) {
return;
}
await completePasskeyLogin(credential);
} catch (error) {
console.error(
"Passkey authentication was not completed",
error
);
}
}
With this approach, you do not need a confusing page asking:
Do you use a password?
OR
Do you use a passkey?
The normal sign-in field can expose compatible stored credentials while password entry remains available.
Step 6: Verify Authentication Before Starting a PHP Session
Passkey login follows the same security principle as registration: browser success is not enough.
Your backend generates a fresh challenge and stores it temporarily.
The authenticator returns an assertion. Your server then verifies it using the public key associated with that credential.
The verification process should check the expected challenge, relying-party ID, origin, required user-presence or verification flags, and cryptographic signature.
Only after successful verification should your PHP application create an authenticated session.
if ($verification->verified) {
session_regenerate_id(true);
$_SESSION['user_id'] =
$verification->userId;
$_SESSION['authenticated_at'] =
time();
}
Regenerating the session identifier after authentication is also important for protecting PHP sessions against session fixation.
Passkeys Do Not Replace Session Security
A strong login mechanism does not protect a badly designed authenticated session.
After authentication, your normal web security controls still matter.
Use secure session cookies.
session_set_cookie_params([
'secure' => true,
'httponly' => true,
'samesite' => 'Lax'
]);
session_start();
You still need:
- Authorization checks
- CSRF protection where applicable
- Session expiration
- Secure logout
- HTTPS
- Rate limiting
- Protection against account enumeration
- Database security
WebAuthn strengthens authentication. It does not automatically secure the rest of your application.
HTTPS Is Not Optional
The Web Authentication API is restricted to secure contexts in normal web deployments.
Production sites therefore need properly configured HTTPS.
This requirement also protects the broader authentication flow from network attackers who could otherwise modify pages, scripts, or requests.
Do not build a production passkey implementation around insecure HTTP and expect to add TLS later.
The Hardest Problem Is Usually Account Recovery
Passkeys can improve login security while accidentally making account recovery weaker.
Consider this design:
Normal Login
Passkey required
Phishing resistant
Recovery
Email address
+
Weak one-time reset flow
An attacker may simply avoid your secure login mechanism and attack the easier recovery path.
OWASP authentication guidance emphasizes protecting authentication and recovery processes together rather than treating recovery as an unrelated feature.
A Better Recovery Strategy
Encourage users to register more than one usable authentication method.
For example:
Primary Passkey
+
Second Passkey
+
Carefully Designed Recovery
If you keep password recovery during migration, review it with the same seriousness as passkey authentication.
Do not make account takeover easier just because a user says they lost a device.
Allow Users to Manage Their Passkeys
A mature implementation needs more than an Add Passkey button.
Users should be able to see credentials connected to their account.
Passkeys
Windows laptop
Added 14 days ago
Last used today
[ Remove ]
Phone
Added 3 months ago
Last used yesterday
[ Remove ]
Security key
Added 6 months ago
Last used 20 days ago
[ Remove ]
Friendly labels and timestamps help users understand what they are deleting.
Before removing the final usable passkey from a passwordless account, make sure the user has another safe way to authenticate.
Do Not Assume Every Passkey Is Hardware-Bound
Developers sometimes describe passkeys as private keys that can never leave a physical device.
That is too broad.
Some credentials may be device-bound, while passkey providers can also support synchronized credentials that work across a user's devices.
OWASP specifically warns developers not to assume credentials are hardware-backed or non-exportable unless they have verified the relevant authenticator properties.
This distinction matters if your threat model requires specific hardware assurance.
Common Implementation Mistakes
Removing Passwords Too Early
If only a small portion of your user base has registered passkeys, removing passwords creates lockouts rather than security.
Run both methods during migration.
Writing WebAuthn Cryptography Yourself
WebAuthn verification involves more than comparing a challenge string.
Use a maintained server-side implementation that understands the specification rather than creating your own partial verifier.
Trusting a User ID From the Browser
During authenticated passkey registration, determine the account from the authenticated PHP session.
Do not accept:
{
"user_id": 1234
}
and blindly attach the new credential to account 1234.
Reusing Challenges
Challenges should be unpredictable and single-use.
Delete or invalidate a challenge after the authentication attempt is processed.
Ignoring Origin and RP ID Verification
The phishing-resistant property of WebAuthn depends heavily on credential scoping and server verification.
Skipping expected-origin or relying-party checks breaks important security assumptions.
Building a Separate Passkey Login Page
A dedicated button can work, but sites migrating existing password users often provide a smoother experience by integrating passkeys into the existing username autofill interface.
Forgetting Recovery Security
Your strongest authentication method is less valuable if account recovery can bypass it with weak verification.
A Practical Rollout Plan
You do not need to migrate the entire authentication system in one release.
Phase 1: Prepare the Backend
- Create the credentials table
- Select a maintained WebAuthn server implementation
- Define your RP ID and expected origins
- Build challenge storage
- Require HTTPS
Phase 2: Add Registration
- Offer Add Passkey in authenticated account settings
- Verify registration responses server-side
- Allow multiple credentials
- Add passkey management controls
Phase 3: Add Login
- Keep password authentication
- Add passkey autofill
- Verify assertions server-side
- Create the same PHP session regardless of authentication method
Phase 4: Improve Recovery
- Encourage multiple passkeys
- Review email recovery
- Protect high-risk account changes
- Log authentication-method changes
Phase 5: Measure Adoption
Track operational metrics rather than assuming users will immediately abandon passwords.
Useful metrics include:
- Percentage of active accounts with a passkey
- Passkey registration failures
- Passkey login success rate
- Password login usage
- Recovery requests
- Credential removal events
Do not store unnecessary biometric or device information to create these metrics.
When Should You Become Passwordless?
Passwordless authentication should be a product decision supported by real adoption data, not a marketing milestone.
Before removing password login, ask:
- Can users reliably authenticate on their normal devices?
- Can users register multiple passkeys?
- Is recovery strong enough?
- Can support staff handle lost-access cases safely?
- Have older browsers and unsupported clients been considered?
- Are high-risk account changes protected?
For many PHP websites, supporting passkeys and passwords together for a long period may be the most practical design.
Advantages and Tradeoffs
Advantages
- Strong resistance to traditional phishing
- No reusable password sent during passkey authentication
- Convenient biometric or device-unlock experience where supported
- Reduced dependence on memorized passwords
- Can coexist with existing authentication
Tradeoffs
- More complicated server implementation than basic passwords
- Recovery requires careful design
- Users may have different passkey providers and devices
- Support teams need new troubleshooting procedures
- Migration takes time for an existing user base
The right goal is not eliminating every password as quickly as possible. The goal is improving authentication without creating new ways for legitimate users to lose access.
FAQ
Can I add passkeys without removing my existing PHP password login?
Yes. Passkeys can be stored as additional credentials linked to the same user account while your existing password login continues to work.
Does PHP provide a built-in WebAuthn authentication function?
PHP provides the server platform you can use for the authentication endpoints, but WebAuthn verification should normally be handled by a maintained WebAuthn server library rather than custom cryptographic code.
Do passkeys require HTTPS?
Yes. The Web Authentication API is available in secure contexts for normal production websites, so your passkey implementation should run over HTTPS.
Can one user have multiple passkeys?
Yes. Your database design should support multiple WebAuthn credentials for one account because users may register passkeys through different devices, providers, or security keys.
Why should I keep password login during migration?
Keeping passwords temporarily prevents account lockouts for users who have not created a passkey or cannot use one on their current device.
Are passkeys resistant to phishing?
Yes. WebAuthn credentials are scoped to the relying party and origin, which prevents a passkey registered for one legitimate site from being used by a lookalike phishing origin.
Should I build WebAuthn signature verification myself?
Usually no. WebAuthn verification has many security-sensitive requirements, so using a maintained server-side implementation is safer than creating a partial custom verifier.
Can weak account recovery undermine passkey security?
Yes. An attacker may target the recovery path instead of the passkey login, so account recovery must receive the same security attention as the primary authentication flow.
Sources
- W3C - Web Authentication: An API for Accessing Public Key Credentials Level 3
- W3C - 2026 WebAuthn Level 3 Candidate Recommendation Update
- MDN Web Docs - Web Authentication API
- MDN Web Docs - Passkeys
- Google for Developers - Server-Side Passkey Registration
- Google for Developers - Server-Side Passkey Authentication
- Google for Developers - Passkey Use Cases and Autofill
- OWASP - Authentication Cheat Sheet
- OWASP - Multifactor Authentication Cheat Sheet


Comments
Loading comments…