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 );
}
}
|