Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support WebAuthn #427

Open
wants to merge 14 commits into
base: master
Choose a base branch
from
Open

Support WebAuthn #427

wants to merge 14 commits into from

Conversation

mcguffin
Copy link

As announced in #423 here's my PR.

Tested with:

  • Windows 10 (VirtualBox)
  • Firefox macOS 10.15 using SoftU2F
  • Chrome macOS using Chrome's native OS support
  • Firefox Android 10
  • Safari on iOS 14.4

Todo (any help on these is very welcome):

  • Test more browsers and more devices
  • Accessibility
  • Make UI less mcguffin and more WordPress

Comment on lines 119 to 124
return $wpdb->query( $wpdb->prepare(
"DELETE FROM $wpdb->usermeta WHERE user_id=%d AND meta_key=%s AND meta_value LIKE %s",
$user_id,
self::PUBKEY_USERMETA_KEY,
'%' . $keyLike . '%'
) ) !== 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, you need to fetch the record first and then delete it by ID. You need the "fetch first" thing to update the meta cache, the same way that delete_metadata() does. Otherwise, get_keys() may yield unexpected result if the site uses an object cache.

*/
public function get_app_id() {

$fqdn = wp_parse_url( network_site_url(), PHP_URL_HOST );
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is probably not critical, but WebAuthn requires an origin here, not just the domain. That is if the webserver for some reason uses a non-standard port for HTTPS (say, 8443), it needs to be included in the AppID.

The best way is probably to copy the logic from get_u2f_app_id() (just omit the protocol).

$key->last_used = false;
$key->tested = false;

if ( false !== $this->key_store->find_key( $user_id, $key->md5id ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is probably not critical, but…

According to the Registration Ceremony, Item 22, the credential must be unique site-wide:

Check that the credentialId is not yet registered to any other user. If registration is requested for a credential that is already registered to a different user, the Relying Party SHOULD fail this registration ceremony, or it MAY decide to accept the registration, e.g. while deleting the older registration.

@mcguffin
Copy link
Author

@sjinks Thanks for review!

@jeffpaul jeffpaul linked an issue Jan 24, 2022 that may be closed by this pull request
@jeffpaul jeffpaul added this to the 0.8.0 milestone Jan 24, 2022
@jeffpaul
Copy link
Member

jeffpaul commented Mar 9, 2022

@sjinks mind re-reviewing after the latest changes from @mcguffin?

@dziudek dziudek mentioned this pull request May 20, 2022
@jeffpaul
Copy link
Member

jeffpaul commented Sep 9, 2022

@sjinks mind re-reviewing after the latest changes from @mcguffin?

@jeffpaul jeffpaul modified the milestones: 0.7.2, 0.8.0 Sep 12, 2022
Copy link
Contributor

@sjinks sjinks left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but I haven't tried to run this locally.

@jeffpaul
Copy link
Member

@sjinks thanks, MUCH appreciated!

@mcguffin any thoughts on the feedback above / ability to work on updates accordingly?

@iandunn
Copy link
Member

iandunn commented Nov 14, 2022

Ah, good point, it looks like even just the .php files is still 17MB 🙈 ( composer install --no-dev && find . -name "*.php" | xargs du -sch). madwizard/webauthn-server is 4.4MB in comparison.

I was also assuming that the 4.x.x branches required PHP 7.x, but it looks like it's actually 8.1, which would probably be too strict even if it was only for this provider. So we'd probably would need to use the 3.3.x branch like Jörn said. But then, we probably can't assume that they'll do security patches for that branch, so that doesn't seem viable to me.

@paulschreiber
Copy link
Contributor

What's the ETA on getting this merged?

@riastradh
Copy link

riastradh commented Mar 4, 2023

Would funding help with getting this and #491 finished, tested, and merged soon, and 0.8.0 released?

@iandunn iandunn modified the milestones: 0.8.0, 0.9.0 Mar 6, 2023
@iandunn
Copy link
Member

iandunn commented Mar 10, 2023

IIRC, that was more than wordpress.org allowed (10 MB).

I looked for the details on that, and didn't see anything in the handbook or on make/Plugins. Jetpack is 222MB unzipped and WordFence is 63MB, but Two Factor is only 1.7MB, so I think we're probably safe to add whatever library we need. We should still prune any files that won't be used, though.

I'm still leaning towards madwizard-org/webauthn-server being the best one that supports PHP 7.x.

flex-wrap: wrap;
-webkit-box-align: last baseline;
-ms-flex-align: last baseline;
align-items: last baseline;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably center align this row.

Suggested change
align-items: last baseline;
align-items: center;
State Before After
Screenshot 2023-04-21 at 11 50 25 AM Screenshot 2023-04-21 at 11 50 16 AM
With Notice Screenshot 2023-04-21 at 11 50 35 AM Screenshot 2023-04-21 at 11 50 51 AM

content: "";
}
.webauthn-key [data-tested="untested"] {
color: #ccc;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
color: #ccc;
color: #ccc;
vertical-align: bottom;

Align the icon better with text.

Before After
Screenshot 2023-04-21 at 11 55 36 AM Screenshot 2023-04-21 at 11 55 12 AM

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needing vertical-align is usually a signal to refactor torwards flexbox. We've come a long way, but browsers still render vertical-align differently in various cases. Life is too short for that.


const login = ( opts, callback ) => {

const { action, payload, _wpnonce } = opts;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

action & _wpnonce are deconstructured but I don't any references.

url: wp.ajax.settings.url,
method: 'post',
data: opts,
success:callback

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
success:callback
success: callback

Copy link

@ravinderk ravinderk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcguffin I left a few suggestions on code. Feel free to ask questions.

I noticed that new files need to be formatted correctly. You cannot catch formatting issues because includes directory is excluded. I think you can remove [this line]( /includes/) to get the correct formatting results locally.

self::PUBKEY_USERMETA_KEY,
'%' . $wpdb->esc_like( $keyLike ) . '%'
) );
foreach ( $found as $key ) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_results function returns array|object|null. Can you validate the result before processing it?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason we are querying all possible results? But returning the result value from the first item in the result.
Do you think we can limit the result to 1?

* @return bool
*/
public function delete_key( $user_id, $keyLike ) {
global $wpdb;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we remove unused global param?

/**
* Enqueue assets for login form.
*/
public function login_enqueue_assets() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we using this function anywhere?

'error' => $err->getMessage(),
)
);
return;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not required because execution will die in wp_send_json . Do you think we can remove this?

$this->key_store->create_key( $user_id, $key );

} catch ( Exception $err ) {
wp_send_json(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Canw e use wp_send_json_success and wp_send_json_error function?

$this->key_store->save_key( $user_id, $key, $key->md5id );
}

wp_send_json(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we replace it with wp_send_json_error and pass WP_Error as in other places?

Comment on lines +480 to +486
if ( $info->response->clientData->challenge
!==
rtrim( strtr( base64_encode( self::arrayToString( $info->originalChallenge ) ), '+/', '-_'), '=')
) {
$this->last_error[ $this->last_call ] = 'info-challenge-mismatch';
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to make sure the challenge sent in prepareAuthenticate needs to be same here. We are checking $info->originalChallenge here, but the problem is that $info is a user supplied value -- see https://www.w3.org/TR/webauthn-2/#sctn-cryptographic-challenges

if ( ! is_array( $info->response->attestationObject ) ) {
$this->last_error[ $this->last_call ] = 'info-response-malformed-property';
return false;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The registration challenge needs to be checked against the challenge sent in prepareRegister -- see https://www.w3.org/TR/webauthn-2/#sctn-cryptographic-challenges

/* check response from key and store as new identity. This is a hex string representing the raw CBOR
attestation object received from the key */

$attData = $this->parseAttestationObject( $info->response->attestationObject );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like there are quite a bit of steps that are not currently implemented here -- https://www.w3.org/TR/webauthn-2/#sctn-registering-a-new-credential. Is that intentional? If so, why?

"SELECT COUNT(*) FROM $wpdb->usermeta WHERE meta_key=%s AND meta_value LIKE %s",
self::PUBKEY_USERMETA_KEY,
'%' . $wpdb->esc_like( serialize( $keyLike ) ) . '%'
) );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to only use core functions like get_user_meta( $user_id, self::PUBKEY_USERMETA_KEY ) and then doing the lookup on PHP? This would avoid uncached queries for sites that make use of object caching plugins.

@jornfranke
Copy link

@kasparsd could you review it or assign someone else for review to unblock this?

@georgestephanis
Copy link
Collaborator

I think the main blocker on the current PR is getting context or resolution on the questions raised in a few of the above reviews.

*/
public function get_app_id() {

$url_parts = wp_parse_url( network_site_url() );
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the ideal workflow on a network that has sites with different domains?

@joshbetz
Copy link
Collaborator

joshbetz commented Jul 6, 2024

It's not working in Safari for me 🤔 I'm able to register my browser with Touch ID, but logging in with Touch ID is not working for me.

@jeffpaul jeffpaul modified the milestones: 0.10.0, 0.11.0 Sep 17, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

U2F support in the future versions of Chrome