1455aa67eSAndreas Gohr<?php 2455aa67eSAndreas Gohr 3455aa67eSAndreas Gohrnamespace dokuwiki; 4455aa67eSAndreas Gohr 5455aa67eSAndreas Gohr/** 6455aa67eSAndreas Gohr * Minimal JWT implementation 7455aa67eSAndreas Gohr */ 8455aa67eSAndreas Gohrclass JWT 9455aa67eSAndreas Gohr{ 10455aa67eSAndreas Gohr 11455aa67eSAndreas Gohr protected $user; 12455aa67eSAndreas Gohr protected $issued; 13455aa67eSAndreas Gohr protected $secret; 14455aa67eSAndreas Gohr 15455aa67eSAndreas Gohr /** 16455aa67eSAndreas Gohr * Create a new JWT object 17455aa67eSAndreas Gohr * 18455aa67eSAndreas Gohr * Use validate() or create() to create a new instance 19455aa67eSAndreas Gohr * 20455aa67eSAndreas Gohr * @param string $user 21455aa67eSAndreas Gohr * @param int $issued 22455aa67eSAndreas Gohr */ 23455aa67eSAndreas Gohr protected function __construct($user, $issued) 24455aa67eSAndreas Gohr { 25455aa67eSAndreas Gohr $this->user = $user; 26455aa67eSAndreas Gohr $this->issued = $issued; 27455aa67eSAndreas Gohr } 28455aa67eSAndreas Gohr 29455aa67eSAndreas Gohr /** 30455aa67eSAndreas Gohr * Load the cookiesalt as secret 31455aa67eSAndreas Gohr * 32455aa67eSAndreas Gohr * @return string 33455aa67eSAndreas Gohr */ 34455aa67eSAndreas Gohr protected static function getSecret() 35455aa67eSAndreas Gohr { 36455aa67eSAndreas Gohr return auth_cookiesalt(false, true); 37455aa67eSAndreas Gohr } 38455aa67eSAndreas Gohr 39455aa67eSAndreas Gohr /** 40455aa67eSAndreas Gohr * Create a new instance from a token 41455aa67eSAndreas Gohr * 42455aa67eSAndreas Gohr * @param $token 43455aa67eSAndreas Gohr * @return self 44455aa67eSAndreas Gohr * @throws \Exception 45455aa67eSAndreas Gohr */ 46455aa67eSAndreas Gohr public static function validate($token) 47455aa67eSAndreas Gohr { 48403d6a9fSAndreas Gohr [$header, $payload, $signature] = sexplode('.', $token, 3, ''); 49455aa67eSAndreas Gohr $signature = base64_decode($signature); 50455aa67eSAndreas Gohr 51455aa67eSAndreas Gohr if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) { 52455aa67eSAndreas Gohr throw new \Exception('Invalid JWT signature'); 53455aa67eSAndreas Gohr } 54455aa67eSAndreas Gohr 55*87603a0aSAndreas Gohr try { 56*87603a0aSAndreas Gohr $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR); 57*87603a0aSAndreas Gohr $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR); 58*87603a0aSAndreas Gohr } catch (\Exception $e) { 59*87603a0aSAndreas Gohr throw new \Exception('Invalid JWT'); 60*87603a0aSAndreas Gohr } 61455aa67eSAndreas Gohr 62455aa67eSAndreas Gohr if (!$header || !$payload || !$signature) { 63455aa67eSAndreas Gohr throw new \Exception('Invalid JWT'); 64455aa67eSAndreas Gohr } 65455aa67eSAndreas Gohr 66455aa67eSAndreas Gohr if ($header['alg'] !== 'HS256') { 67455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT algorithm'); 68455aa67eSAndreas Gohr } 69455aa67eSAndreas Gohr if ($header['typ'] !== 'JWT') { 70455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT type'); 71455aa67eSAndreas Gohr } 72455aa67eSAndreas Gohr if ($payload['iss'] !== 'dokuwiki') { 73455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT issuer'); 74455aa67eSAndreas Gohr } 75455aa67eSAndreas Gohr if (isset($payload['exp']) && $payload['exp'] < time()) { 76455aa67eSAndreas Gohr throw new \Exception('JWT expired'); 77455aa67eSAndreas Gohr } 78455aa67eSAndreas Gohr 79455aa67eSAndreas Gohr $user = $payload['sub']; 80403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 81455aa67eSAndreas Gohr if (!file_exists($file)) { 82455aa67eSAndreas Gohr throw new \Exception('JWT not found, maybe it expired?'); 83455aa67eSAndreas Gohr } 84455aa67eSAndreas Gohr 85403d6a9fSAndreas Gohr if(file_get_contents($file) !== $token) { 86403d6a9fSAndreas Gohr throw new \Exception('JWT invalid, maybe it expired?'); 87403d6a9fSAndreas Gohr } 88403d6a9fSAndreas Gohr 89455aa67eSAndreas Gohr return new self($user, $payload['iat']); 90455aa67eSAndreas Gohr } 91455aa67eSAndreas Gohr 92455aa67eSAndreas Gohr /** 93455aa67eSAndreas Gohr * Create a new instance from a user 94455aa67eSAndreas Gohr * 95455aa67eSAndreas Gohr * Loads an existing token if available 96455aa67eSAndreas Gohr * 97455aa67eSAndreas Gohr * @param $user 98455aa67eSAndreas Gohr * @return self 99455aa67eSAndreas Gohr */ 100455aa67eSAndreas Gohr public static function fromUser($user) 101455aa67eSAndreas Gohr { 102403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 103455aa67eSAndreas Gohr 104455aa67eSAndreas Gohr if (file_exists($file)) { 105455aa67eSAndreas Gohr try { 106455aa67eSAndreas Gohr return self::validate(io_readFile($file)); 107455aa67eSAndreas Gohr } catch (\Exception $ignored) { 108455aa67eSAndreas Gohr } 109455aa67eSAndreas Gohr } 110455aa67eSAndreas Gohr 111455aa67eSAndreas Gohr $token = new self($user, time()); 112455aa67eSAndreas Gohr $token->save(); 113455aa67eSAndreas Gohr return $token; 114455aa67eSAndreas Gohr } 115455aa67eSAndreas Gohr 116455aa67eSAndreas Gohr 117455aa67eSAndreas Gohr /** 118455aa67eSAndreas Gohr * Get the JWT token for this instance 119455aa67eSAndreas Gohr * 120455aa67eSAndreas Gohr * @return string 121455aa67eSAndreas Gohr */ 122455aa67eSAndreas Gohr public function getToken() 123455aa67eSAndreas Gohr { 124455aa67eSAndreas Gohr $header = [ 125455aa67eSAndreas Gohr 'alg' => 'HS256', 126455aa67eSAndreas Gohr 'typ' => 'JWT', 127455aa67eSAndreas Gohr ]; 128455aa67eSAndreas Gohr $header = base64_encode(json_encode($header)); 129455aa67eSAndreas Gohr $payload = [ 130455aa67eSAndreas Gohr 'iss' => 'dokuwiki', 131455aa67eSAndreas Gohr 'sub' => $this->user, 132455aa67eSAndreas Gohr 'iat' => $this->issued, 133455aa67eSAndreas Gohr ]; 134455aa67eSAndreas Gohr $payload = base64_encode(json_encode($payload)); 135455aa67eSAndreas Gohr $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 136455aa67eSAndreas Gohr $signature = base64_encode($signature); 137455aa67eSAndreas Gohr return "$header.$payload.$signature"; 138455aa67eSAndreas Gohr } 139455aa67eSAndreas Gohr 140455aa67eSAndreas Gohr /** 141455aa67eSAndreas Gohr * Save the token for the user 142455aa67eSAndreas Gohr * 143455aa67eSAndreas Gohr * Resets the issued timestamp 144455aa67eSAndreas Gohr */ 145455aa67eSAndreas Gohr public function save() 146455aa67eSAndreas Gohr { 147455aa67eSAndreas Gohr $this->issued = time(); 148403d6a9fSAndreas Gohr io_saveFile(self::getStorageFile($this->user), $this->getToken()); 149455aa67eSAndreas Gohr } 150455aa67eSAndreas Gohr 151455aa67eSAndreas Gohr /** 152455aa67eSAndreas Gohr * Get the user of this token 153455aa67eSAndreas Gohr * 154455aa67eSAndreas Gohr * @return string 155455aa67eSAndreas Gohr */ 156455aa67eSAndreas Gohr public function getUser() 157455aa67eSAndreas Gohr { 158455aa67eSAndreas Gohr return $this->user; 159455aa67eSAndreas Gohr } 160455aa67eSAndreas Gohr 161455aa67eSAndreas Gohr /** 162455aa67eSAndreas Gohr * Get the issued timestamp of this token 163455aa67eSAndreas Gohr * 164455aa67eSAndreas Gohr * @return int 165455aa67eSAndreas Gohr */ 166455aa67eSAndreas Gohr public function getIssued() 167455aa67eSAndreas Gohr { 168455aa67eSAndreas Gohr return $this->issued; 169455aa67eSAndreas Gohr } 170403d6a9fSAndreas Gohr 171403d6a9fSAndreas Gohr /** 172403d6a9fSAndreas Gohr * Get the storage file for this token 173403d6a9fSAndreas Gohr * 174403d6a9fSAndreas Gohr * Tokens are stored to be able to invalidate them 175403d6a9fSAndreas Gohr * 176403d6a9fSAndreas Gohr * @param string $user The user the token is for 177403d6a9fSAndreas Gohr * @return string 178403d6a9fSAndreas Gohr */ 179403d6a9fSAndreas Gohr public static function getStorageFile($user) 180403d6a9fSAndreas Gohr { 181403d6a9fSAndreas Gohr return getCacheName($user, '.token'); 182403d6a9fSAndreas Gohr } 183455aa67eSAndreas Gohr} 184