Sync your WordPress posts to standard.site records on your PDS
1<?php
2declare(strict_types=1);
3/**
4 * DPoP (Demonstration of Proof of Possession) support using oauth2-dpop library.
5 *
6 * @package Wireservice
7 */
8
9namespace Wireservice;
10
11if (! defined('ABSPATH')) {
12 exit;
13}
14
15use danielburger1337\OAuth2\DPoP\DPoPProofFactory;
16use danielburger1337\OAuth2\DPoP\Encoder\WebTokenFrameworkDPoPTokenEncoder;
17use danielburger1337\OAuth2\DPoP\Model\AccessTokenModel;
18use Jose\Component\Core\AlgorithmManager;
19use Jose\Component\Core\JWK;
20use Jose\Component\Signature\Algorithm\ES256;
21use Symfony\Component\Clock\NativeClock;
22
23class DPoP
24{
25 /**
26 * Supported algorithms for AT Protocol.
27 *
28 * @var array
29 */
30 private const SUPPORTED_ALGORITHMS = ["ES256"];
31
32 /**
33 * Cached factory instances keyed by JWK hash.
34 *
35 * @var array<string, DPoPProofFactory>
36 */
37 private static array $factories = [];
38
39 /**
40 * Cached JWK thumbprints keyed by JWK hash.
41 *
42 * @var array<string, string>
43 */
44 private static array $thumbprints = [];
45
46 /**
47 * Generate a DPoP proof JWT.
48 *
49 * @param array $jwk The JWK to sign with (must include private key 'd').
50 * @param string $method The HTTP method (GET, POST, etc.).
51 * @param string $url The full URL being requested.
52 * @param string|null $nonce Optional server-provided nonce.
53 * @param string|null $access_token Optional access token for ath claim.
54 * @return string|false The DPoP proof JWT or false on failure.
55 */
56 public static function generate_proof(
57 array $jwk,
58 string $method,
59 string $url,
60 ?string $nonce = null,
61 ?string $access_token = null,
62 ): string|false {
63 try {
64 $factory = self::get_factory($jwk);
65
66 // Store nonce if provided (for the library to pick up).
67 if ($nonce !== null) {
68 self::store_nonce($jwk, $url, $nonce);
69 }
70
71 // Create the proof.
72 $bindTo = null;
73 if ($access_token !== null) {
74 $thumbprint = self::get_thumbprint($jwk);
75 $bindTo = new AccessTokenModel($access_token, $thumbprint);
76 }
77
78 $proof = $factory->createProof(
79 strtoupper($method),
80 $url,
81 self::SUPPORTED_ALGORITHMS,
82 $bindTo,
83 );
84
85 return $proof->proof;
86 } catch (\Throwable $e) {
87 wp_trigger_error(__METHOD__, "DPoP proof generation failed: " . $e->getMessage());
88 return false;
89 }
90 }
91
92 /**
93 * Store a nonce received from a server response.
94 *
95 * @param array $jwk The JWK used in the request.
96 * @param string $url The request URL.
97 * @param string $nonce The nonce from the response header.
98 */
99 public static function store_nonce(array $jwk, string $url, string $nonce): void
100 {
101 $factory = self::get_factory($jwk);
102 $jwkInterface = $factory->getJwkToBind(self::SUPPORTED_ALGORITHMS);
103 $factory->storeNextNonce($nonce, $jwkInterface, $url);
104 }
105
106 /**
107 * Get or create a DPoPProofFactory for the given JWK.
108 *
109 * @param array $jwk The JWK array.
110 * @return DPoPProofFactory The factory instance.
111 */
112 private static function get_factory(array $jwk): DPoPProofFactory
113 {
114 $key = md5(wp_json_encode($jwk));
115
116 if (!isset(self::$factories[$key])) {
117 $jwtJwk = new JWK($jwk);
118 $algorithmManager = new AlgorithmManager([new ES256()]);
119 $encoder = new WebTokenFrameworkDPoPTokenEncoder($jwtJwk, $algorithmManager);
120 $nonceStorage = new NonceStorage();
121 $clock = new NativeClock();
122
123 self::$factories[$key] = new DPoPProofFactory(
124 $clock,
125 $encoder,
126 $nonceStorage,
127 );
128 }
129
130 return self::$factories[$key];
131 }
132
133 /**
134 * Get the JWK thumbprint.
135 *
136 * @param array $jwk The JWK array.
137 * @return string The thumbprint.
138 */
139 private static function get_thumbprint(array $jwk): string
140 {
141 $key = md5(wp_json_encode($jwk));
142
143 if (!isset(self::$thumbprints[$key])) {
144 $jwtJwk = new JWK($jwk);
145 self::$thumbprints[$key] = $jwtJwk->thumbprint("sha256");
146 }
147
148 return self::$thumbprints[$key];
149 }
150
151}