HEX
Server: Apache/2.4.6 (CentOS) OpenSSL/1.0.2k-fips PHP/7.4.30
System: Linux iZj6c1151k3ad370bosnmsZ 3.10.0-1160.76.1.el7.x86_64 #1 SMP Wed Aug 10 16:21:17 UTC 2022 x86_64
User: root (0)
PHP: 7.4.30
Disabled: NONE
Upload Files
File: /var/www/html/breadsecret.com/wp-content/plugins/login-with-ajax/passkeys/passkeys-server.php
<?php
namespace Login_With_AJAX\Passkeys;

use lbuchs\WebAuthn\WebAuthn;

class Server {
	
	public static $current_user_id = 0;
	
	public static function init() {
		// add ajax action for receiving passkeys
		add_action('wp_ajax_lwa_passkeys', array( static::class, 'ajax_passkeys' ) );
		add_action('wp_ajax_nopriv_lwa_passkeys', array( static::class, 'ajax_passkeys' ) );
		add_action('wp_ajax_nopriv_lwa_passkey_login', array( static::class, 'ajax_passkeys' ) );
	}
	
	public static function ajax_passkeys() {
		// check nonce, if so then process the request
		$nonce_check = false;
		if ( !empty($_REQUEST['fn'])  ) {
			// check the nonce against users
			if( in_array( $_REQUEST['fn'], array('getCreateArgs', 'processCreate', 'edit', 'delete') ) ) {
				$nonce_check = \LoginWithAjax::check_user_and_nonce('lwa_passkeys_' . $_REQUEST['fn'] . '-', null, false);
			} else {
				$nonce_check = check_ajax_referer('lwa_passkeys_' . $_REQUEST['fn'], 'nonce', false);
			}
		}
		if ( !$nonce_check ) {
			wp_send_json( array( 'success' => false, 'result' => false, 'msg' => 'Invalid nonce' ) );
		}
		
		require_once 'webauthn/WebAuthn.php';
		try {
			session_start();
			
			// read get argument and post body
			$fn = filter_input(INPUT_GET, 'fn');
			$requireResidentKey = true; // client-side discoverable = true, otherwise we need to get userhandle
			$userVerification = 'preferred'; // ['required', 'preferred', 'discouraged']
			
			/*
			$userId = filter_input(INPUT_GET, 'userId', FILTER_SANITIZE_SPECIAL_CHARS);
			$userName = filter_input(INPUT_GET, 'userName', FILTER_SANITIZE_SPECIAL_CHARS);
			$userDisplayName = filter_input(INPUT_GET, 'userDisplayName', FILTER_SANITIZE_SPECIAL_CHARS);
			
			$userId = preg_replace('/[^0-9a-f]/i', '', $userId);
			$userName = preg_replace('/[^0-9a-z]/i', '', $userName);
			$userDisplayName = preg_replace('/[^0-9a-z öüäéèàÖÜÄÉÈÀÂÊÎÔÛâêîôû]/i', '', $userDisplayName);
			*/
			
			$post = trim(file_get_contents('php://input'));
			if ($post) {
				$post = json_decode($post, null, 512, JSON_THROW_ON_ERROR);
			}
				
				// Formats -  get none for now
				$formats = ['none'];
				/*
				$formats[] = 'android-key';
				$formats[] = 'android-safetynet';
				$formats[] = 'apple';
				$formats[] = 'fido-u2f';
				$formats[] = 'none';
				$formats[] = 'packed';
				$formats[] = 'tpm';
				*/
				
				// get domain of site
				$rpId = preg_replace('/^https?:\/\//', '', get_site_url());
				$rpId = preg_replace('/\/.*$/', '', $rpId); // just the domain we need
				
				// types selected on front end - all for nwo
				$typeUsb = true;
				$typeNfc = true;
				$typeBle = true;
				$typeInt = false;
				$typeHyb = true;
				
				// cross-platform: true, if type internal is not allowed
				//                 false, if only internal is allowed
				//                 null, if internal and cross-platform is allowed
				$crossPlatformAttachment = null;
				/**
				if (($typeUsb || $typeNfc || $typeBle || $typeHyb) && !$typeInt) {
					$crossPlatformAttachment = true;
					
				} else if (!$typeUsb && !$typeNfc && !$typeBle && !$typeHyb && $typeInt) {
					$crossPlatformAttachment = false;
				}
				 */
				
				// new Instance of the server library.
				// make sure that $rpId is the domain name.
				$WebAuthn = new WebAuthn('WebAuthn Library', $rpId, $formats);
				
				// add root certificates to validate new registrations
				/*
				$WebAuthn->addRootCertificates('rootCertificates/solo.pem');
				$WebAuthn->addRootCertificates('rootCertificates/apple.pem');
				$WebAuthn->addRootCertificates('rootCertificates/yubico.pem');
				$WebAuthn->addRootCertificates('rootCertificates/hypersecu.pem');
				$WebAuthn->addRootCertificates('rootCertificates/globalSign.pem');
				$WebAuthn->addRootCertificates('rootCertificates/googleHardware.pem');
				$WebAuthn->addRootCertificates('rootCertificates/microsoftTpmCollection.pem');
				$WebAuthn->addRootCertificates('rootCertificates/mds');
				*/
			
			if ($fn === 'getCreateArgs') {
				// nonce passed, so we can assume user has permission to create a passkey (for example, via 2FA in grace mode)
				$user = $nonce_check; /* @var \WP_User $user */
				// create user id and save to db
				$userId = get_user_meta( $user->ID, 'lwa_passkey_id', true );
				if( empty( $userId ) ) {
					// generate new hexadecimal string that is unique in wp_usermeta for key lwa_passkey_id
					global $wpdb;
					do {
						$userId = wp_generate_uuid4();
						$userId = preg_replace('/[^0-9a-z]/i', '', $userId);
						$results = $wpdb->get_results('SELECT meta_value FROM '. $wpdb->usermeta . ' WHERE meta_key="lwa_passkey_id" AND meta_value="'. $userId .'"');
					} while ( !empty($results) );
					add_user_meta( $user->ID, 'lwa_passkey_id', $userId, true );
				}
				
				$userName = $user->user_login;
				$userDisplayName = $user->display_name;
				
				// create args and save challenge for new method
				$createArgs = $WebAuthn->getCreateArgs(\hex2bin($userId), $userName, $userDisplayName, 60*4, $requireResidentKey, $userVerification, $crossPlatformAttachment);
				
				//\LoginWithAjax::update_user_meta( $user->ID, 'passkeys[challenge]', $WebAuthn->getChallenge() );
				$_SESSION['challenge'] = $WebAuthn->getChallenge();
				
				// return create args
				wp_send_json( $createArgs );
			
			} else if ($fn === 'processCreate') {
				// nonce passed, so we can assume user has permission to create a passkey (for example, via 2FA in grace mode)
				$user = $nonce_check; /* @var \WP_User $user */
				$clientDataJSON = base64_decode($post->clientDataJSON);
				$attestationObject = base64_decode($post->attestationObject);
				
				// find challenge from user ID and make sure this lines up with user that's logged in to create the
				$challenge = $_SESSION['challenge'] ?? '';
				if( empty($challenge) ) {
					wp_send_json( array( 'result' => false, 'success' => false, 'msg' => 'No challenge found for user' ) );
				}
				
				// processCreate returns data to be stored for future logins.
				// in this example we store it in the php session.
				// Normaly you have to store the data in a database connected
				// with the user name.
				$passkey = $WebAuthn->processCreate($clientDataJSON, $attestationObject, $challenge, $userVerification === 'required', true, false);
				
				// add user infos
				$userId = get_user_meta( $user->ID, 'lwa_passkey_id', true);
				$passkey->userId = $userId;
				$passkey->userName = $user->user_login;
				$passkey->userDisplayName = $user->display_name;
				
				// add browser name and OS from user agent string
				$passkey->label = 'New Device';
				
				// add create date and last used empty date
				$passkey->created = time();
				$passkey->last_used = '';
				
				// convert bin data to hex
				$passkey->AAGUID = bin2hex( $passkey->AAGUID );
				$passkey->credentialId = bin2hex( $passkey->credentialId );
				
				// save to passkeys user meta
				if ( static::update_passkey( $user->ID, $passkey ) ) {
					wp_send_json( array(
						'result' => true,
						'success' => true,
						'message' => 'Passkey was successfully registered.',
						'data' => array(
							'label' => $passkey->label,
							'last_used' => 0,
							'created' => wp_date( get_option('date_format'), $passkey->created ),
							'id' => $passkey->credentialId,
							'rpId' => $passkey->rpId,
							'multidomain' => static::is_multidomain( $user->ID ),
						)
					));
				} else {
					wp_send_json( array( 'result' => false, 'success' => false, 'error' =>  sprintf( esc_html__('Passkey could not be %s, contact support for assistance.', 'login-with-ajax-pro'), esc_html__('edited') ) ) );
				}
				
			} else if ($fn === 'getGetArgs') {
				
				$ids = array();
				if( !$requireResidentKey ) {
					$user_id = static::get_user_id( bin2hex($post->userHandle) );
					
					if( !$user_id ) {
						wp_send_json( array( 'result' => false, 'success' => false, 'error' => 'No user found for passkey' ) );
					}
					
					$passkeys = \LoginWithAjax::get_user_meta( $user_id, 'passkeys', array() );
					
					if ( count($passkeys) === 0 ) {
						throw new \Exception('no registrations for userId ' . bin2hex($post->userHandle));
					}
					
					$ids = array_keys($passkeys);
				}
				$getArgs = $WebAuthn->getGetArgs($ids, 60*4, $typeUsb, $typeNfc, $typeBle, $typeHyb, $typeInt, $userVerification);
				
				// save challange again, we may need it later
				$_SESSION['challenge'] = $WebAuthn->getChallenge();
				
				wp_send_json( $getArgs );
				
			} else if ($fn === 'processGet') {
				
				$clientDataJSON = base64_decode($post->clientDataJSON);
				$authenticatorData = base64_decode($post->authenticatorData);
				$signature = base64_decode($post->signature);
				$userHandle = base64_decode($post->userHandle);
				$id = bin2hex(base64_decode($post->id));
				$challenge = $_SESSION['challenge'] ?? '';
				$credentialPublicKey = null;
				
				$user_id = static::get_user_id( bin2hex($userHandle) );
				
				if( !$user_id ) {
					wp_send_json( array( 'result' => false, 'success' => false, 'error' => 'No user found for passkey' ) );
				}
				
				// looking up correspondending public key of the credential id, potential improvement is also validate that only ids of the given user name, although users can modify this in their browser
				$passkey = static::get_passkey( $user_id, $id );
				
				if ($passkey === null) {
					throw new \Exception('Public Key for credential ID not found!');
				}
				
				// if we have resident key, we have to verify that the userHandle is the provided userId at registration
				if ( $requireResidentKey && bin2hex($userHandle) !== $passkey->userId ) {
					throw new \Exception('User ID of passkey does not match our records.');
				}
				
				// process the get request. throws WebAuthnException if it fails
				$WebAuthn->processGet($clientDataJSON, $authenticatorData, $signature, $passkey->credentialPublicKey, $challenge, null, $userVerification === 'required');
				
				// save last-used date
				$passkey->last_used = time();
				static::update_passkey( $user_id, $passkey );
				
				// log in user
				if ( !empty($post->testing) ) {
					// this is a test request, we just return true
					wp_send_json( array( 'result' => true, 'success' => true, 'message' => esc_html__('Your passkey is successfully registered and verified!', 'login-with-ajax-pro'), 'testing' => true ) );
				} else {
					remove_all_filters('lwa_authenticate'); //allow other LWA things to authenticate and trigger 2FA
					remove_all_filters('lwa_login'); //allow other LWA things to authenticate and trigger 2FA
					remove_all_filters('lwa_ajax_2FA');
					add_filter( 'ws_plugin__s2member_login_redirect', '__return_false' );
					remove_all_filters('login_redirect');
					$user = get_user_by( 'id', $user_id );
					add_filter( 'authenticate', function( $user, $username ) { return get_user_by( 'login', $username ); }, 11, 3 );    // hook in earlier than other callbacks to short-circuit them
					$user = wp_signon( array( 'user_login' => $user->user_login, 'remember' => true ) );
					remove_all_filters( 'authenticate', 'allow_programmatic_login', 11 );
					if ( is_a( $user, 'WP_User' ) ) {
						wp_set_current_user( $user->ID, $user->user_login );
					}
					// handle redirect_to
					if( !empty($post->redirect_to) ) $_REQUEST['redirect_to'] = $post->redirect_to;
					// return login result like LWA does
					wp_send_json( \LoginWithAjax::login_result( $user ) );
				}
			} elseif ($fn === 'delete') {
				// nonce passed, so we can assume user has permission to create a passkey (for example, via 2FA in grace mode)
				$user = $nonce_check; /* @var \WP_User $user */
				// delete the passkey
				if( $user && (get_current_user_id() === $user->ID || !is_user_logged_in() || current_user_can('edit_users') ) ) {
					if( static::delete_passkey( $user->ID, $post->id ) ) {
						wp_send_json( array( 'result' => true, 'success' => true, 'message' => 'Passkey deleted.' ) );
					} else {
						wp_send_json( array( 'result' => false, 'success' => false, 'error' => sprintf( esc_html__('Passkey could not be %s, contact support for assistance.', 'login-with-ajax-pro'), esc_html__('deleted') ) ) );
					}
				} else {
					// edge case, no translation
					wp_send_json( array( 'result' => false, 'success' => false, 'error' => 'No passkey found for removal.' ) );
				}
			} elseif ($fn === 'edit') {
				// nonce passed, so we can assume user has permission to create a passkey (for example, via 2FA in grace mode)
				$user = $nonce_check; /* @var \WP_User $user */
				// update the passkey label
				if( $user && (get_current_user_id() === $user->ID || !is_user_logged_in() || current_user_can('edit_users') ) ) {
					$passkey = static::get_passkey( $user->ID, $post->id );
					if( $passkey ) {
						$passkey->label = wp_kses_data($post->label);
						if ( static::update_passkey( $user->ID, $passkey ) ) {
							wp_send_json( array( 'result' => true, 'success' => true, 'message' => 'Passkey name updated.', 'label' => $passkey->label ) );
						} else {
							wp_send_json( array( 'result' => false, 'success' => false, 'error' => sprintf( esc_html__('Passkey could not be %s, contact support for assistance.', 'login-with-ajax-pro'), esc_html__('edited') ) ) );
						}
					} else {
						// not found, edge so no translateion
						wp_send_json( array( 'result' => false, 'success' => false, 'error' => 'No passkey found for editing.') );
					}
				} else {
					// no permission, edge so no translation
					wp_send_json( array( 'result' => false, 'success' => false, 'error' => 'You do not have permission to edit this passkey.' ) );
				}
			}
			
		} catch ( \Throwable $ex) {
			wp_send_json( array('result' => false, 'success' => false, 'error' => $ex->getMessage(), 'testing' => !empty($post->testing) ) );
		}
	}
	
	/**
	 * Updates or adds a passkey to the passkeys list of given User ID
	 * @param int $user_id
	 * @param \stdClass $passkey
	 * @throws \Exception
	 *
	 * @return bool
	 */
	public static function update_passkey( $user_id, $passkey ) {
		// base64 encode it all to be safe and save so it's serializable
		$passkeys = get_user_meta( $user_id, 'lwa_passkeys', true );
		$passkeys = $passkeys ?: array();
		$current_passkey = !empty($passkeys[ $passkey->credentialId ]) ? $passkeys[ $passkey->credentialId ] : null;
		$passkeys[ $passkey->credentialId ] = $passkey;
		if( !$current_passkey ) {
			// new passkey, check there's no duplicate AAGUID
			foreach ( $passkeys as $pk ) {
				if( $pk->AAGUID === $passkey->AAGUID && $pk->credentialId !== $passkey->credentialId ) {
					if( $pk->rpId === $passkey->rpId ) {
						// only if registered for different URLs too, in case of multidomain installations
						throw new \Exception( sprintf( __('This device has already been registered as "%s".', 'login-with-ajax-pro'), $pk->label ));
					}
				}
			}
		}
		$result = update_user_meta( $user_id, 'lwa_passkeys', $passkeys );
		return $result || $current_passkey == $passkey;
	}
	
	public static function get_passkey( $user_id, $passkey_id ) {
		$passkey = null;
		$passkeys = get_user_meta( $user_id, 'lwa_passkeys', true );
		if ( !empty($passkeys[ $passkey_id ]) ) {
			$passkey = $passkeys[ $passkey_id ];
		}
		return $passkey;
	}
	
	public static function delete_passkey( $user_id, $passkey_id ) {
		// delete the passkey
		$passkeys = get_user_meta( $user_id, 'lwa_passkeys', true );
		if( !empty($passkeys[ $passkey_id ]) ) {
			unset($passkeys[ $passkey_id ]);
			update_user_meta( $user_id, 'lwa_passkeys', $passkeys );
			return true;
		}
		return false;
	}
	
	public static function get_user_id ( $userId ) {
		// find the user id from the passkey $userId
		global $wpdb;
		$sql = $wpdb->prepare('SELECT user_id FROM ' . $wpdb->usermeta . ' WHERE meta_key = "lwa_passkey_id" AND meta_value = %s', $userId);
		$user_id = $wpdb->get_var($sql);
		return $user_id;
	}
	
	/**
	 * Checks if the user has multiple passkeys with same device but registered to different domains on same site.
	 * @param $user_id
	 *
	 * @return bool
	 */
	public static function is_multidomain( $user_id ) {
		// go through passkeys and check if we have duplicate AAGUIDs for different rpIds, if so we're in multidomain mode
		$passkeys = get_user_meta( $user_id, 'lwa_passkeys', true );
		$passkeys = $passkeys ?: array();
		$multidomain = false;
		$AAGUIDs = array();
		foreach( $passkeys as $passkey ) {
			if( !empty($passkey->AAGUID) ) {
				if ( !empty( $AAGUIDs[ $passkey->AAGUID ] ) && $AAGUIDs[ $passkey->AAGUID ] !== $passkey->rpId ) {
					$multidomain = true;
					break;
				}
				$AAGUIDs[ $passkey->AAGUID ] = $passkey->rpId;
			}
		}
		return $multidomain;
	}
}
Server::init();