Skip to content

Instantly share code, notes, and snippets.

@kepawni
Created February 9, 2022 09:08
Show Gist options
  • Save kepawni/45c9b37dd64a4327ff7806147b1368df to your computer and use it in GitHub Desktop.
Save kepawni/45c9b37dd64a4327ff7806147b1368df to your computer and use it in GitHub Desktop.
Simple, dependency-free, HS256-based JWT implementation for PHP 8.1 and below.
/**
* Create and validate JWT tokens based on HS256 (HMAC with SHA-256) without requiring any particular PHP extensions or
* 3rd party libraries. Works with PHP 8.1 and below. Supports issuer and timestamp validation, result memoization and
* access to the payload. Have a look at the other great libraries out there, but if you need to validate self-issued
* access tokens and want to keep it simple, this one might be just right.
*/
class SimpleJWT
{
private array $data = [];
private string $header;
private array $validity = [];
/**
* In order to validate tokens you need to instantiate this class with the same $secret and $issuer as you did for
* generating the token.
*
* @param string $secret The secret for generating the signature.
* @param string|null $issuer A string to tell different issuers apart.
*/
public function __construct(private string $secret, private ?string $issuer = null)
{
$this->header = $this->base64UrlEncode($this->toJson(['alg' => 'HS256', 'typ' => 'JWT']));
}
/**
* Creates an access token to be used with HTTP Bearer Authentication. It is recommended to use short-lived tokens
* and renew them after a few minutes (or on every request).
*
* @param string $subject What this token is about, e.g. a user ID or even JSON with more details.
* @param int $lifetimeInSeconds For how long you would like this token to be valid (default 5 minutes).
*
* @return string The access token.
*/
public function create(string $subject, int $lifetimeInSeconds = 300): string
{
$time = time();
$data = implode('.', [
$this->header,
$this->base64UrlEncode($this->toJson([
'iat' => $time,
'iss' => $this->issuer,
'exp' => $time + $lifetimeInSeconds,
'sub' => $subject,
])),
]);
return implode('.', [
$data,
$this->base64UrlEncode(hash_hmac('sha256', $data, $this->secret, true)),
]);
}
/**
* Extracts the payload from the token and returns it as an object. You can then access the subject (sub) or the
* issuer (iss) like this: ```$extractedPayload->sub``` or ```$extractedPayload->iss```.
* The result will be memoized.
*
* @param string $token The access token.
*
* @return object|null The extracted data or null when the token was invalid.
*/
public function extractPayload(string $token): ?object
{
return $this->validate($token) ? $this->data[$this->extractSignature($token)] : null;
}
/**
* Validates the given token, checking format, header equality, signature, issuer equality and timestamps.
* The result will be memoized.
*
* @param string $token The access token.
*
* @return bool Whether the token is (still) valid.
*/
public function validate(string $token): bool
{
$signature = $this->extractSignature($token);
return $this->validity[$signature] = $this->validity[$signature]
?? (substr_count($token, '.') === 2)
&& ([$h, $p, $s] = explode('.', $token))
&& $h === $this->header
&& $this->base64UrlEncode(hash_hmac('sha256', $h . "." . $p, $this->secret, true)) === $s
&& ($data = json_decode($this->base64UrlDecode($p)))
&& ($data->iss ?? null) === $this->issuer
&& ($data->exp ?? 0) >= time()
&& ($data->iat ?? INF) < ($data->exp ?? 0)
&& ($this->data[$signature] = $data);
}
private function base64UrlDecode(string $data): string
{
return base64_decode(strtr(str_pad($data, intval(ceil(strlen($data) / 4) * 4), '='), '-_', '+/'));
}
private function base64UrlEncode(string $data): string
{
return strtr(base64_encode($data), ['+' => '-', '/' => '_', '=' => '']);
}
private function extractSignature(string $token): string
{
return substr($token, 1 + (strrpos($token, '.') ?: -1));
}
private function toJson(array $data): string
{
return json_encode(
array_filter($data, fn($i) => !is_null($i)),
JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE
) ?: '[]';
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment