Laravel AT Protocol Client (alpha & unstable)
1<?php
2
3namespace SocialDept\AtpClient\Auth;
4
5use Firebase\JWT\JWT;
6use phpseclib3\Crypt\EC;
7use SocialDept\AtpClient\Contracts\KeyStore;
8use SocialDept\AtpClient\Data\DPoPKey;
9
10class DPoPKeyManager
11{
12 public function __construct(
13 protected KeyStore $keyStore
14 ) {}
15
16 /**
17 * Generate new ES256 key pair
18 */
19 public function generateKey(string $sessionId): DPoPKey
20 {
21 // Generate P-256 elliptic curve key pair
22 $privateKey = EC::createKey('secp256r1');
23 $publicKey = $privateKey->getPublicKey();
24 $keyId = $this->generateKeyId($publicKey);
25
26 $dpopKey = new DPoPKey($privateKey, $publicKey, $keyId);
27
28 // Store the key
29 $this->keyStore->store($sessionId, $dpopKey);
30
31 return $dpopKey;
32 }
33
34 /**
35 * Create DPoP proof JWT
36 */
37 public function createProof(
38 DPoPKey $key,
39 string $method,
40 string $url,
41 string $nonce = '',
42 ?string $accessToken = null
43 ): string {
44 $now = time();
45
46 $payload = [
47 'jti' => bin2hex(random_bytes(16)),
48 'htm' => $method,
49 'htu' => $url,
50 'iat' => $now,
51 'exp' => $now + 60, // 1 minute validity
52 ];
53
54 // Only include nonce if provided (first request may not have one)
55 if ($nonce !== '') {
56 $payload['nonce'] = $nonce;
57 }
58
59 if ($accessToken) {
60 $payload['ath'] = $this->hashAccessToken($accessToken);
61 }
62
63 $header = [
64 'typ' => 'dpop+jwt',
65 'alg' => 'ES256',
66 'jwk' => $key->getPublicJwk(),
67 ];
68
69 return JWT::encode(
70 payload: $payload,
71 key: $key->toPEM(),
72 alg: 'ES256',
73 head: $header
74 );
75 }
76
77 /**
78 * Hash access token for DPoP proof
79 */
80 protected function hashAccessToken(string $token): string
81 {
82 return rtrim(strtr(base64_encode(hash('sha256', $token, true)), '+/', '-_'), '=');
83 }
84
85 /**
86 * Generate key ID from public key
87 */
88 protected function generateKeyId($publicKey): string
89 {
90 $jwk = $publicKey->toString('JWK');
91
92 return hash('sha256', $jwk);
93 }
94}