'HS256', 'typ' => 'JWT']; $payload = array_merge([ 'iss' => self::issuer(), 'iat' => time(), 'exp' => time() + self::ttl(), ], $claims); $h = self::b64u(json_encode($header, JSON_UNESCAPED_UNICODE)); $p = self::b64u(json_encode($payload, JSON_UNESCAPED_UNICODE)); $sig = hash_hmac('sha256', $h . '.' . $p, self::secret(), true); return $h . '.' . $p . '.' . self::b64u($sig); } public static function decode(string $token): ?array { $parts = explode('.', $token); if (count($parts) !== 3) { return null; } [$h, $p, $s] = $parts; $expected = self::b64u(hash_hmac('sha256', $h . '.' . $p, self::secret(), true)); if (!hash_equals($expected, $s)) { return null; } $payload = json_decode(self::b64uDecode($p), true); if (!is_array($payload)) { return null; } if (!isset($payload['iss']) || $payload['iss'] !== self::issuer()) { return null; } if (isset($payload['exp']) && $payload['exp'] < time()) { return null; } return $payload; } private static function b64u(string $bin): string { return rtrim(strtr(base64_encode($bin), '+/', '-_'), '='); } private static function b64uDecode(string $b64): string { $b64 = strtr($b64, '-_', '+/'); $pad = 4 - (strlen($b64) % 4); if ($pad < 4) { $b64 .= str_repeat('=', $pad); } return base64_decode($b64); } }