assets
1 day ago
format
1 day ago
helpers
1 day ago
class-ajax-passkeys.php
1 day ago
class-api-register.php
1 day ago
class-api-signin.php
1 day ago
class-attestation-object.php
1 day ago
class-authenticate-server.php
1 day ago
class-authenticator-data.php
1 day ago
class-byte-buffer.php
1 day ago
class-chor-decoder.php
1 day ago
class-passkeys-endpoints.php
1 day ago
class-passkeys-user-profile.php
1 day ago
class-passkeys-wizard-steps.php
1 day ago
class-passkeys.php
1 day ago
class-pending-2fa-helper.php
1 day ago
class-source-repository.php
1 day ago
class-web-authn-exception.php
1 day ago
class-web-authn.php
1 day ago
index.php
1 day ago
class-authenticate-server.php
184 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Responsible for the API endpoints |
| 4 | * |
| 5 | * @package wp-2fa |
| 6 | * @since 3.0.0 |
| 7 | * @copyright 2026 Melapress |
| 8 | * @license https://www.apache.org/licenses/LICENSE-2.0 Apache License 2.0 |
| 9 | * @link https://wordpress.org/plugins/wp-2fa/ |
| 10 | */ |
| 11 | |
| 12 | declare(strict_types=1); |
| 13 | |
| 14 | namespace WP2FA\Passkeys; |
| 15 | |
| 16 | use WP2FA\Methods\Passkeys\Web_Authn; |
| 17 | |
| 18 | /** |
| 19 | * Authentication Server. |
| 20 | */ |
| 21 | if ( ! class_exists( '\WP2FA\Passkeys\Authentication_Server' ) ) { |
| 22 | |
| 23 | /** |
| 24 | * Web Authentication Server |
| 25 | * |
| 26 | * @since 3.0.0 |
| 27 | */ |
| 28 | class Authentication_Server { |
| 29 | |
| 30 | /** |
| 31 | * Create Attestation Request for registration. |
| 32 | * |
| 33 | * @param \WP_User $user - Current User. |
| 34 | * @param string|null $challenge - Challenge string. |
| 35 | * @param bool $is_usb - Is USB authenticator. |
| 36 | * |
| 37 | * @return PublicKeyCredentialCreationOptions |
| 38 | * |
| 39 | * @since 3.0.0 |
| 40 | */ |
| 41 | public static function create_attestation_request( \WP_User $user, ?string $challenge = null, bool $is_usb = false ) { |
| 42 | |
| 43 | // $fingerprint = base64_encode( self::generate_fingerprint() ); |
| 44 | |
| 45 | // $minutes = intval( 5 ); |
| 46 | |
| 47 | // $date = ( new \DateTime() )->add( new \DateInterval( 'PT' . $minutes . 'M' ) ); |
| 48 | // $expire_date = $date->format( 'Y-m-d H:i:s' ); |
| 49 | |
| 50 | $challenge = base64_encode( random_bytes( 32 ) ); |
| 51 | |
| 52 | // Store challenge in User meta. |
| 53 | \update_user_meta( $user->ID, WP_2FA_PREFIX . 'passkey_challenge', $challenge ); |
| 54 | |
| 55 | $user_id = (string) \get_current_user_id(); |
| 56 | |
| 57 | $first_name = get_user_meta( $user_id, 'first_name', true ); |
| 58 | $last_name = get_user_meta( $user_id, 'last_name', true ); |
| 59 | |
| 60 | $full_name = trim( $first_name . ' ' . $last_name ); |
| 61 | |
| 62 | if ( empty( $full_name ) ) { |
| 63 | $full_name = get_the_author_meta( 'user_email', $user_id ); |
| 64 | } |
| 65 | |
| 66 | $username = trim( $full_name ); |
| 67 | |
| 68 | $options = array( |
| 69 | 'challenge' => $challenge, |
| 70 | 'rp' => array( |
| 71 | 'id' => Web_Authn::get_relying_party_id(), |
| 72 | 'name' => Web_Authn::get_relying_party_id(), |
| 73 | ), |
| 74 | 'user' => array( |
| 75 | 'id' => base64_encode( $user_id ), |
| 76 | 'name' => $username, |
| 77 | 'displayName' => $username, |
| 78 | ), |
| 79 | 'pubKeyCredParams' => array( |
| 80 | array( |
| 81 | 'type' => 'public-key', |
| 82 | 'alg' => -7, |
| 83 | ), |
| 84 | array( |
| 85 | 'type' => 'public-key', |
| 86 | 'alg' => -257, |
| 87 | ), |
| 88 | ), |
| 89 | 'attestation' => 'none', |
| 90 | 'extensions' => array( |
| 91 | 'credProps' => true, |
| 92 | ), |
| 93 | 'timeout' => intval( 5 ) * 60 * 1000, |
| 94 | |
| 95 | // 'authenticatorSelection' => array( |
| 96 | // 'residentKey' => 'discouraged', |
| 97 | // 'requiredResidentKey' => false, |
| 98 | // 'userVerification' => 'required', |
| 99 | // 'authenticatorAttachment' => 'cross-platform', |
| 100 | |
| 101 | // ), |
| 102 | ); |
| 103 | |
| 104 | if ( $is_usb ) { |
| 105 | $options['authenticatorSelection'] = array( |
| 106 | 'authenticatorAttachment' => 'cross-platform', |
| 107 | 'residentKey' => 'discouraged', |
| 108 | 'userVerification' => 'required', |
| 109 | ); |
| 110 | } else { |
| 111 | $options['authenticatorSelection'] = array( |
| 112 | 'authenticatorAttachment' => 'platform', |
| 113 | 'residentKey' => 'required', |
| 114 | 'userVerification' => 'required', |
| 115 | ); |
| 116 | } |
| 117 | // $options['authenticatorSelection'] = array( |
| 118 | |
| 119 | // 'residentKey' => 'required', |
| 120 | // 'residentKey' => 'discouraged', |
| 121 | // 'userVerification' => 'required', |
| 122 | // 'authenticatorAttachment' => 'cross-platform', |
| 123 | // ); |
| 124 | |
| 125 | $public_key_credentials = Source_Repository::find_all_for_user( $user ); |
| 126 | |
| 127 | if ( ! empty( $public_key_credentials ) ) { |
| 128 | $exclude_credentials = array(); |
| 129 | |
| 130 | foreach ( $public_key_credentials as $public_key_credential ) { |
| 131 | $exclude_credentials[] = array( |
| 132 | 'type' => 'public-key', |
| 133 | 'id' => $public_key_credential['credential_id'], |
| 134 | ); |
| 135 | } |
| 136 | |
| 137 | $options['excludeCredentials'] = $exclude_credentials; |
| 138 | } |
| 139 | |
| 140 | return $options; |
| 141 | } |
| 142 | |
| 143 | /** |
| 144 | * Generates the finger print for authentication |
| 145 | * |
| 146 | * @return string |
| 147 | * |
| 148 | * @since 3.0.0 |
| 149 | */ |
| 150 | private static function generate_fingerprint() { |
| 151 | |
| 152 | $user_agent = sanitize_text_field( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? 'unknown' ) ); |
| 153 | $ip_address = self::get_ip_address() ?? 'unknown'; |
| 154 | $accept_language = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'unknown' ) ); |
| 155 | $accept_encoding = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT_ENCODING'] ?? 'unknown' ) ); |
| 156 | |
| 157 | $fingerprint_data = $user_agent . '|' . $ip_address . '|' . $accept_language . '|' . $accept_encoding; |
| 158 | |
| 159 | $fingerprint_hash = hash( 'sha256', $fingerprint_data ); |
| 160 | |
| 161 | return $fingerprint_hash; |
| 162 | } |
| 163 | |
| 164 | /** |
| 165 | * Collects the ip address of the user |
| 166 | * |
| 167 | * @return string |
| 168 | * |
| 169 | * @since 3.0.0 |
| 170 | */ |
| 171 | public static function get_ip_address() { |
| 172 | if ( ! empty( $_SERVER['HTTP_CLIENT_IP'] ) ) { |
| 173 | $ip = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_CLIENT_IP'] ) ); |
| 174 | } elseif ( ! empty( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) { |
| 175 | $ip = \rest_is_ip_address( trim( current( preg_split( '/,/', \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_X_FORWARDED_FOR'] ) ) ) ) ) ); |
| 176 | } else { |
| 177 | $ip = \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); |
| 178 | } |
| 179 | |
| 180 | return (string) $ip; |
| 181 | } |
| 182 | } |
| 183 | } |
| 184 |