dokuwiki/inc/JWT.php

185 lines
4.3 KiB
PHP

<?php
namespace dokuwiki;
/**
* Minimal JWT implementation
*/
class JWT
{
protected $user;
protected $issued;
protected $secret;
/**
* Create a new JWT object
*
* Use validate() or create() to create a new instance
*
* @param string $user
* @param int $issued
*/
protected function __construct($user, $issued)
{
$this->user = $user;
$this->issued = $issued;
}
/**
* Load the cookiesalt as secret
*
* @return string
*/
protected static function getSecret()
{
return auth_cookiesalt(false, true);
}
/**
* Create a new instance from a token
*
* @param $token
* @return self
* @throws \Exception
*/
public static function validate($token)
{
[$header, $payload, $signature] = sexplode('.', $token, 3, '');
$signature = base64_decode($signature);
if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) {
throw new \Exception('Invalid JWT signature');
}
try {
$header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR);
$payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR);
} catch (\Exception $e) {
throw new \Exception('Invalid JWT', $e->getCode(), $e);
}
if (!$header || !$payload || !$signature) {
throw new \Exception('Invalid JWT');
}
if ($header['alg'] !== 'HS256') {
throw new \Exception('Unsupported JWT algorithm');
}
if ($header['typ'] !== 'JWT') {
throw new \Exception('Unsupported JWT type');
}
if ($payload['iss'] !== 'dokuwiki') {
throw new \Exception('Unsupported JWT issuer');
}
if (isset($payload['exp']) && $payload['exp'] < time()) {
throw new \Exception('JWT expired');
}
$user = $payload['sub'];
$file = self::getStorageFile($user);
if (!file_exists($file)) {
throw new \Exception('JWT not found, maybe it expired?');
}
if (file_get_contents($file) !== $token) {
throw new \Exception('JWT invalid, maybe it expired?');
}
return new self($user, $payload['iat']);
}
/**
* Create a new instance from a user
*
* Loads an existing token if available
*
* @param $user
* @return self
*/
public static function fromUser($user)
{
$file = self::getStorageFile($user);
if (file_exists($file)) {
try {
return self::validate(io_readFile($file));
} catch (\Exception $ignored) {
}
}
$token = new self($user, time());
$token->save();
return $token;
}
/**
* Get the JWT token for this instance
*
* @return string
*/
public function getToken()
{
$header = [
'alg' => 'HS256',
'typ' => 'JWT',
];
$header = base64_encode(json_encode($header));
$payload = [
'iss' => 'dokuwiki',
'sub' => $this->user,
'iat' => $this->issued,
];
$payload = base64_encode(json_encode($payload, JSON_THROW_ON_ERROR));
$signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
$signature = base64_encode($signature);
return "$header.$payload.$signature";
}
/**
* Save the token for the user
*
* Resets the issued timestamp
*/
public function save()
{
$this->issued = time();
io_saveFile(self::getStorageFile($this->user), $this->getToken());
}
/**
* Get the user of this token
*
* @return string
*/
public function getUser()
{
return $this->user;
}
/**
* Get the issued timestamp of this token
*
* @return int
*/
public function getIssued()
{
return $this->issued;
}
/**
* Get the storage file for this token
*
* Tokens are stored to be able to invalidate them
*
* @param string $user The user the token is for
* @return string
*/
public static function getStorageFile($user)
{
return getCacheName($user, '.token');
}
}