Created
February 9, 2022 09:08
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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