-
Notifications
You must be signed in to change notification settings - Fork 153
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
base: master
Are you sure you want to change the base?
Support WebAuthn #427
Conversation
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; |
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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 ) ) { |
There was a problem hiding this comment.
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.
@sjinks Thanks for review! |
There was a problem hiding this 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.
Ah, good point, it looks like even just the I was also assuming that the |
What's the ETA on getting this merged? |
Would funding help with getting this and #491 finished, tested, and merged soon, and 0.8.0 released? |
I looked for the details on that, and didn't see anything in the handbook or on make/Plugins. Jetpack is I'm still leaning towards |
flex-wrap: wrap; | ||
-webkit-box-align: last baseline; | ||
-ms-flex-align: last baseline; | ||
align-items: last baseline; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
content: ""; | ||
} | ||
.webauthn-key [data-tested="untested"] { | ||
color: #ccc; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
success:callback | |
success: callback |
There was a problem hiding this 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 ) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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() { |
There was a problem hiding this comment.
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; |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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( |
There was a problem hiding this comment.
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?
if ( $info->response->clientData->challenge | ||
!== | ||
rtrim( strtr( base64_encode( self::arrayToString( $info->originalChallenge ) ), '+/', '-_'), '=') | ||
) { | ||
$this->last_error[ $this->last_call ] = 'info-challenge-mismatch'; | ||
return false; | ||
} |
There was a problem hiding this comment.
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; | ||
} |
There was a problem hiding this comment.
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 ); |
There was a problem hiding this comment.
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 ) ) . '%' | ||
) ); |
There was a problem hiding this comment.
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.
@kasparsd could you review it or assign someone else for review to unblock this? |
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() ); |
There was a problem hiding this comment.
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?
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. |
As announced in #423 here's my PR.
Tested with:
Todo (any help on these is very welcome):