summaryrefslogtreecommitdiffstats
path: root/includes/class-cepis-woo-token.php
blob: 1d1b9f404294abf28b528112fb9309aa561c7dfa (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
<?php
/**
 * The token that guards the server-side proxy route.
 *
 * @package CepisWooCommerce
 */

declare(strict_types=1);

defined( 'ABSPATH' ) || exit;

/**
 * A short-lived nonce for the proxy route, derived the way WordPress derives
 * its own but without binding to a login session.
 *
 * The bundled widget fetches with `credentials: "omit"` — deliberately, since
 * a CEP lookup has no business carrying the shopper's cookies. The proxy
 * therefore never sees an authentication cookie and every request reaches it
 * anonymous, whether or not the shopper is logged in. A cookie-bound
 * {@see wp_create_nonce()} would embed a session token in the page that the
 * request could not present, so it would verify for guests and fail for
 * everyone with an account: the worst possible failure mode, invisible in
 * testing and broken in production.
 *
 * What the token has to defend against is the only threat this endpoint
 * actually has — a third party pointing their own site at this store's proxy
 * and using it as a free CEP gateway. A value tied to the site's salts and to
 * {@see wp_nonce_tick()} does exactly that: it cannot be produced without the
 * salts, and it stops working within a day.
 *
 * Same lifetime as a WordPress nonce: the current tick and the one before it
 * are both accepted, so a checkout page held open overnight keeps working.
 */
final class Cepis_Woo_Token {

	/**
	 * Namespaces the hash, so a token cannot be replayed against anything else
	 * on the site that hashes with the `nonce` salt.
	 */
	private const ACTION = 'cepis_woo_proxy_lookup';

	/**
	 * Mint the token to embed in the checkout page.
	 */
	public static function create(): string {
		return self::hash( wp_nonce_tick( self::ACTION ) );
	}

	/**
	 * Check a token presented by the browser.
	 *
	 * Compared with {@see hash_equals()} so that a wrong token takes the same
	 * time to reject as a right one.
	 *
	 * @param string $token Token as it arrived in the request.
	 */
	public static function verify( string $token ): bool {
		if ( '' === $token ) {
			return false;
		}

		$tick = wp_nonce_tick( self::ACTION );

		return hash_equals( self::hash( $tick ), $token )
			|| hash_equals( self::hash( $tick - 1 ), $token );
	}

	/**
	 * Derive the token for one tick.
	 *
	 * @param float $tick Nonce tick to derive from.
	 */
	private static function hash( float $tick ): string {
		return substr( wp_hash( $tick . '|' . self::ACTION, 'nonce' ), -12, 10 );
	}
}