<?php namespace WPForms\SetupWizard; use WP_REST_Request; /** * Session token authentication for the Setup Wizard. * * Issues an HMAC-SHA512 token bound to the issuing admin's user ID. The token * is stored in a 1-hour transient as `[ 'token' => string, 'user_id' => int ]`, * refreshed on every authenticated request, and verified with `hash_equals()`. * Validating a token returns the bound user ID so the REST layer can hydrate * the cross-origin request via `wp_set_current_user()`. * * @since 2.0.0 */ class Auth { /** * Transient name storing the active token payload. * * @since 2.0.0 * * @var string */ private const TRANSIENT_TOKEN = 'wpforms_setup_wizard_token'; /** * Token lifetime in seconds. * * @since 2.0.0 * * @var int */ private const TTL = HOUR_IN_SECONDS; /** * Header name the SPA uses to send the token back. * * @since 2.0.0 * * @var string */ private const HEADER = 'X-WPForms-Setup-Wizard-Token'; /** * Issue a token bound to the given user ID. * * Reuses the existing token when the stored transient still belongs to the * same user, so a second launch in the same session yields a stable token. * * @since 2.0.0 * * @return string The hashed token, raw value to send to the SPA. */ public function generate_token(): string { $user_id = get_current_user_id(); if ( $user_id <= 0 ) { return ''; } $stored = get_transient( self::TRANSIENT_TOKEN ); if ( is_array( $stored ) && ! empty( $stored['token'] ) && $user_id === (int) ( $stored['user_id'] ?? 0 ) ) { $token = (string) $stored['token']; } else { $token = wp_generate_password( 64, false ); } set_transient( self::TRANSIENT_TOKEN, [ 'token' => $token, 'user_id' => $user_id, ], self::TTL ); return $this->sign( $user_id, $token ); } /** * Refresh the stored token's TTL without changing its value. * * @since 2.0.0 * * @param array $stored_token Token currently in use. */ private function refresh( array $stored_token ): void { set_transient( self::TRANSIENT_TOKEN, $stored_token, self::TTL ); } /** * Validate a REST request and return the bound user ID. * * Reads the token from the request header, compares with `hash_equals()`, * and returns the user ID if valid. Returns 0 on failure. * * @since 2.0.0 * * @param WP_REST_Request $request REST request. * * @return int Bound user ID, or 0 if the token is missing/invalid. */ public function validate_request( WP_REST_Request $request ): int { $token = (string) $request->get_header( self::HEADER ); if ( $token === '' ) { return 0; } $stored = get_transient( self::TRANSIENT_TOKEN ); if ( ! is_array( $stored ) || empty( $stored['token'] ) ) { return 0; } $user_id = (int) ( $stored['user_id'] ?? 0 ); if ( $user_id <= 0 ) { return 0; } $expected = $this->sign( $user_id, (string) $stored['token'] ); if ( ! hash_equals( $expected, $token ) ) { return 0; } $this->refresh( $stored ); return $user_id; } /** * Revoke the active token (e.g., on wizard completion). * * @since 2.0.0 */ public function revoke(): void { delete_transient( self::TRANSIENT_TOKEN ); } /** * Build the HMAC-SHA512 signature for a token payload. * * @since 2.0.0 * * @param int $user_id User the token is bound to. * @param string $token Per-request entropy. * * @return string */ private function sign( int $user_id, string $token ): string { return hash_hmac( 'sha512', $user_id . '|' . $token, wp_salt() ); } }