Back to Blog
Attack Prevention

Passkeys Offer Both Benefits and New Attack Surfaces

March 12, 2024

Share
Be careful when implementing passkeys. They can offer a more secure alternative to passwords when implemented correctly, but provide many ways for implementers to shoot themselves in the foot.

Intro

Passkeys are a recent attempt to mitigate the risks associated with password-based authentication. Built on the WebAuthn standard, passkeys can offer a more secure alternative to passwords when implemented correctly.

However, they are far from a magic bullet. Passkey implementation opens up new attack surfaces in related endpoints, and services relying on them will require additional layers of defense to prevent account takeovers.

In this post, we delve into common attack vectors in backend endpoints being added to serve passkey flows, and considerations for protecting these endpoints using hCaptcha tokens.

This is not an exhaustive list of all possible risks and implementation errors, but rather a summary of the ones we have seen most often.

Benefits of Passkeys

1. Enhanced Security: Passkeys are in theory more resistant to phishing attacks, as the authentication process involves a direct interaction between the user's device and the server, although in practice we have seen many successful phishing attacks against passkeys in the wild. Passkeys also eliminate the risk of password reuse and weak passwords (if passwords are indeed disabled for the user when a passkey is added) as they are unique to each site or app.

2. Improved User Experience: With passkeys, authentication can in principle be performed with a simple biometric gesture, such as a fingerprint scan or facial recognition, making the login process quick and effortless. In practice, a large percentage of users will continue to use password managers, storing their passkeys in the manager and often using a password to unlock the manager.

3. Reduced Password Management Overhead: In theory, implementing passkeys could eliminate the need for password management on the server-side. Websites and applications would no longer need to store and manage user passwords securely. This reduces the risk of password breaches and simplifies the authentication infrastructure. However, in reality most implementations we have reviewed continue to store the user's password, whether for account recovery or as a parallel login mechanism.


Perils of Passkeys

1. User Adoption: One of the main challenges in implementing passkeys is user adoption. We expect passwords will still be preferred by some users, and most systems will need to support passwords and passkey authentication in parallel indefinitely.

2. Device Compatibility: Passkeys rely on specific hardware and software requirements, and not everyone can meet those requirements today.

3. Recovery and Account Portability: In the event of device loss or failure, recovering access to accounts secured with passkeys can be challenging. Implementing secure recovery mechanisms, such as backup codes or trusted device registration, is essential to ensure users can regain access to their accounts, but opens up a lot of surface area for attacks at the same time.

4. Backend devs are not cryptographers: Today's popular libraries are too low-level, and browser/client implementations have uneven feature support. The current state of the art requires devs to understand e.g. nuances of use counters, devise logic to handle some features only on clients where that feature exists, etc. We've observed even very good backend developers are often getting this wrong.


Backend Endpoints in the Passkey Login Flow

1. Registration Endpoint:

```python
@app.route('/register', methods=['POST'])
def register():
   # Verify hCaptcha token to prevent automated attacks
   hcaptcha_token = request.form.get('hcaptcha_token')
   if not verify_hcaptcha(hcaptcha_token):
       return jsonify({'error': 'Invalid hCaptcha token'}), 400

   # Process user registration
   user_email = request.form.get('email')
   # ... Perform webauthn creation logic ...

   return jsonify({'message': 'Registration successful'}), 200
```

Commentary: Passkey generation is trivial to automate. This means the registration endpoint should be protected with an hCaptcha token (e.g. a passive/invisible token if you are an hCaptcha Enterprise customer) to prevent automated attacks and spam registrations. Verifying the hCaptcha token provides a risk evaluation on the new user and increases the cost of attack.

2. Login Endpoint:

```python
@app.route('/login', methods=['POST'])
def login():

   # WRONG but common pattern:
   #
   # email = request.json.get('email') .. then immediately check if email exists.
   #
   # Doing this BEFORE we've done a token validation or webauthn validation
   # enables DB query DDoS, user enumeration attacks, side channel attacks, etc.

   # Verify hCaptcha token to prevent automated attacks:
   # This is the FIRST thing we should do on the endpoint.
   hcaptcha_token = request.json.get('hcaptcha_token')
   if not verify_hcaptcha(hcaptcha_token):
       return jsonify({'error': 'Invalid hCaptcha token'}), 400

   # Retrieve the WebAuthn assertion from the request
   webauthn_assertion = request.json.get('webauthn_assertion')

   # Verify the WebAuthn assertion
   if not verify_webauthn_assertion(webauthn_assertion):
       return jsonify({'error': 'Invalid WebAuthn assertion'}), 401

   # Authenticate the user
   user_id = get_user_id_from_assertion(webauthn_assertion)
   # ... Perform authentication logic ...

   return jsonify({'message': 'Login successful'}), 200
```

Commentary: The login endpoint requires an hCaptcha token, since the WebAuthn assertion itself cannot provide endpoint protection, only authentication. The server verifies the WebAuthn assertion to ensure the authenticity of the user.

3. User Info Endpoint:

```python
@app.route('/user-info', methods=['GET'])
def user_info():

   # Verify hCaptcha token to prevent user email enumeration and side channel attacks.
   # This is the FIRST thing we should do on the endpoint.
   hcaptcha_token = request.json.get('hcaptcha_token')
   if not verify_hcaptcha(hcaptcha_token):
       return jsonify({'error': 'Invalid hCaptcha token'}), 400

   # Retrieve user information from session
   user_id = get_authenticated_user_id()


   user_email = get_user_email(user_id)
   # ... Retrieve additional user information ...

   return jsonify({'email': user_email, ...}), 200
```

Commentary: The user info endpoint should be protected with an hCaptcha token to prevent user email enumeration attacks. Attackers may attempt to credential stuff or enumerate user emails by making repeated requests to this endpoint. Verifying the hCaptcha token ensures that the request comes from a legitimate user and helps mitigate such attacks.

4. Logout Endpoint:

```python
@app.route('/logout', methods=['POST'])
def logout():
   # Retrieve the authenticated user's session
   user_session = get_authenticated_user_session()

   # Token verification is optional on logout. However:
   # if you do not have an hCaptcha token here, you MUST return early
   # before making any additional DB calls.
   if not user_session:
       return jsonify({'error': 'Not logged in'}), 400

   # Invalidate the user's session
   invalidate_user_session(user_session)

   return jsonify({'message': 'Logout successful'}), 200
```

Commentary: The logout endpoint does not necessarily require a hCaptcha token so long as you have structured the control flow to prevent abuse and have strong rate limiting in place. However, an hCaptcha token may be required for additional protection against distributed DDoS depending on the structure of your app and how much backend work a logout event triggers for you.

Please note this is not an exhaustive list of endpoints you may create to serve the passkey flow. Reset paths, available-auth-type paths, and others are also vulnerable to similar abuse and will require protection.

Other Common Issues in Passkey Implementations

There are also other anti-patterns to keep in mind when implementing passkeys.

For example, we have seen some backend engineers use the raw integer userid as the webauthn assertion ID, exposing the database row ID directly to the end user. This may open up enumeration attacks in other parts of your application, or attacks on the login flow itself depending on the logic used.

If you are using UUID4 primary keys this is less of a concern, but best practice remains to hash or encrypt this value rather than exposing it directly to the end user.

Summary

Implementing passkeys offers some benefits in enhanced security and improved user experience. However, it is crucial to remediate risks during implementation. Aside from the issues discussed above, fallback mechanisms like SMS 2FA can easily be abused for things like SMS tolling attacks, and also require hCaptcha token protection.

It is essential to evaluate the security design of each endpoint. Endpoints susceptible to automated attacks or user enumeration, like registration and login endpoints, should be protected with a service like hCaptcha to prevent abuse.

Similarly, you should not assume account takeovers (ATOs) will end if users switch to passkeys. Using services like hCaptcha Enterprise Account Defense that seamlessly integrate with passkeys and other auth mechanisms will let you apply simple, consistent business logic to the account lifecycle no matter the authentication method.

Thank you

We hope this was helpful, and please do reach out to us with any questions or comments.

Subscribe to our newsletter

Stay up to date on the latest trends in cyber security. No spam, promise.
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.

Back to blog