$this->algorithm, 'typ' => 'JWT', ]; if (empty($payload['exp'])) { $payload['exp'] = time() + $this->expireSeconds; } $signInput = join('.', [ static::base64Encode(static::jsonEncode($header)), static::base64Encode(static::jsonEncode($payload)), ]); if ($algorithm) { $this->useAlgorithm($algorithm); } $signature = hash_hmac($this->algorithm, $signInput, $secret, true); return $signInput . '.' . static::base64Encode($signature); } /** * Parse a JWT token created by this helper * * @param string $token * @param string $secret * * @return object * @throws Exception */ public function parse(string $token, string $secret): object { $atoms = explode('.', $token); if (count($atoms) != 3) { throw new Exception('Invalid JWT Token'); } else { [$header64, $payload64, $signature64] = $atoms; $signature = $this->base64Decode($signature64); if (!$signature) { throw new Exception('Invalid JWT Signature'); } elseif ($payloadJson = $this->base64Decode($payload64)) { $hash = hash_hmac($this->algorithm, "{$header64}.{$payload64}", $secret, true); if (hash_equals($hash, $signature)) { $payload = $this->jsonDecode($payloadJson); if (empty($payload->exp) == false && $payload->exp < time()) { throw new Exception('Expired JWT token'); } return $payload; } else { throw new Exception('Invalid JWT Token'); } } else { throw new Exception('Invalid JWT Payload'); } } } /** * @param string $algorithm * * @return $this * @throws Exception */ public function useAlgorithm(string $algorithm): self { if (in_array($algorithm, hash_algos(), true)) { $this->algorithm = $algorithm; return $this; } throw new Exception('Invalid Hash Algorithm'); } /** * Base64 encoding that is url safe * * @param string $input * * @return string */ protected function base64Encode(string $input): string { return str_replace( '=', '', strtr( base64_encode($input), '+/', '-_' ) ); } /** * Restore URL safe base64 string * * @param string $input * * @return string */ protected function base64Decode(string $input): string { $remainder = strlen($input) % 4; if ($remainder) { $padding = 4 - $remainder; $input .= str_repeat('=', $padding); } // JWT segments use base64url encoding, so translate to standard Base64 before decoding. $decoded = base64_decode(strtr($input, '-_', '+/'), true); return false === $decoded ? '' : $decoded; } /** * JSON Encode without escape characters * * @param array $input * * @return string */ protected function jsonEncode(array $input): string { return json_encode($input, JSON_UNESCAPED_SLASHES); } /** * @param ?string $input * * @return ?object * @throws Exception */ protected function jsonDecode(?string $input): ?object { $result = json_decode((string)$input, false, 512, JSON_BIGINT_AS_STRING); if ($result && is_array($result)) { $result = (object)$result; } if ($input && empty($result)) { throw new Exception('Invalid JSON'); } return $result ?: null; } }