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 { 48*403d6a9fSAndreas 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 55455aa67eSAndreas Gohr $header = json_decode(base64_decode($header), true); 56455aa67eSAndreas Gohr $payload = json_decode(base64_decode($payload), true); 57455aa67eSAndreas Gohr 58455aa67eSAndreas Gohr if (!$header || !$payload || !$signature) { 59455aa67eSAndreas Gohr throw new \Exception('Invalid JWT'); 60455aa67eSAndreas Gohr } 61455aa67eSAndreas Gohr 62455aa67eSAndreas Gohr if ($header['alg'] !== 'HS256') { 63455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT algorithm'); 64455aa67eSAndreas Gohr } 65455aa67eSAndreas Gohr if ($header['typ'] !== 'JWT') { 66455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT type'); 67455aa67eSAndreas Gohr } 68455aa67eSAndreas Gohr if ($payload['iss'] !== 'dokuwiki') { 69455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT issuer'); 70455aa67eSAndreas Gohr } 71455aa67eSAndreas Gohr if (isset($payload['exp']) && $payload['exp'] < time()) { 72455aa67eSAndreas Gohr throw new \Exception('JWT expired'); 73455aa67eSAndreas Gohr } 74455aa67eSAndreas Gohr 75455aa67eSAndreas Gohr $user = $payload['sub']; 76*403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 77455aa67eSAndreas Gohr if (!file_exists($file)) { 78455aa67eSAndreas Gohr throw new \Exception('JWT not found, maybe it expired?'); 79455aa67eSAndreas Gohr } 80455aa67eSAndreas Gohr 81*403d6a9fSAndreas Gohr if(file_get_contents($file) !== $token) { 82*403d6a9fSAndreas Gohr throw new \Exception('JWT invalid, maybe it expired?'); 83*403d6a9fSAndreas Gohr } 84*403d6a9fSAndreas Gohr 85455aa67eSAndreas Gohr return new self($user, $payload['iat']); 86455aa67eSAndreas Gohr } 87455aa67eSAndreas Gohr 88455aa67eSAndreas Gohr /** 89455aa67eSAndreas Gohr * Create a new instance from a user 90455aa67eSAndreas Gohr * 91455aa67eSAndreas Gohr * Loads an existing token if available 92455aa67eSAndreas Gohr * 93455aa67eSAndreas Gohr * @param $user 94455aa67eSAndreas Gohr * @return self 95455aa67eSAndreas Gohr */ 96455aa67eSAndreas Gohr public static function fromUser($user) 97455aa67eSAndreas Gohr { 98*403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 99455aa67eSAndreas Gohr 100455aa67eSAndreas Gohr if (file_exists($file)) { 101455aa67eSAndreas Gohr try { 102455aa67eSAndreas Gohr return self::validate(io_readFile($file)); 103455aa67eSAndreas Gohr } catch (\Exception $ignored) { 104455aa67eSAndreas Gohr } 105455aa67eSAndreas Gohr } 106455aa67eSAndreas Gohr 107455aa67eSAndreas Gohr $token = new self($user, time()); 108455aa67eSAndreas Gohr $token->save(); 109455aa67eSAndreas Gohr return $token; 110455aa67eSAndreas Gohr } 111455aa67eSAndreas Gohr 112455aa67eSAndreas Gohr 113455aa67eSAndreas Gohr /** 114455aa67eSAndreas Gohr * Get the JWT token for this instance 115455aa67eSAndreas Gohr * 116455aa67eSAndreas Gohr * @return string 117455aa67eSAndreas Gohr */ 118455aa67eSAndreas Gohr public function getToken() 119455aa67eSAndreas Gohr { 120455aa67eSAndreas Gohr $header = [ 121455aa67eSAndreas Gohr 'alg' => 'HS256', 122455aa67eSAndreas Gohr 'typ' => 'JWT', 123455aa67eSAndreas Gohr ]; 124455aa67eSAndreas Gohr $header = base64_encode(json_encode($header)); 125455aa67eSAndreas Gohr $payload = [ 126455aa67eSAndreas Gohr 'iss' => 'dokuwiki', 127455aa67eSAndreas Gohr 'sub' => $this->user, 128455aa67eSAndreas Gohr 'iat' => $this->issued, 129455aa67eSAndreas Gohr ]; 130455aa67eSAndreas Gohr $payload = base64_encode(json_encode($payload)); 131455aa67eSAndreas Gohr $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 132455aa67eSAndreas Gohr $signature = base64_encode($signature); 133455aa67eSAndreas Gohr return "$header.$payload.$signature"; 134455aa67eSAndreas Gohr } 135455aa67eSAndreas Gohr 136455aa67eSAndreas Gohr /** 137455aa67eSAndreas Gohr * Save the token for the user 138455aa67eSAndreas Gohr * 139455aa67eSAndreas Gohr * Resets the issued timestamp 140455aa67eSAndreas Gohr */ 141455aa67eSAndreas Gohr public function save() 142455aa67eSAndreas Gohr { 143455aa67eSAndreas Gohr $this->issued = time(); 144*403d6a9fSAndreas Gohr io_saveFile(self::getStorageFile($this->user), $this->getToken()); 145455aa67eSAndreas Gohr } 146455aa67eSAndreas Gohr 147455aa67eSAndreas Gohr /** 148455aa67eSAndreas Gohr * Get the user of this token 149455aa67eSAndreas Gohr * 150455aa67eSAndreas Gohr * @return string 151455aa67eSAndreas Gohr */ 152455aa67eSAndreas Gohr public function getUser() 153455aa67eSAndreas Gohr { 154455aa67eSAndreas Gohr return $this->user; 155455aa67eSAndreas Gohr } 156455aa67eSAndreas Gohr 157455aa67eSAndreas Gohr /** 158455aa67eSAndreas Gohr * Get the issued timestamp of this token 159455aa67eSAndreas Gohr * 160455aa67eSAndreas Gohr * @return int 161455aa67eSAndreas Gohr */ 162455aa67eSAndreas Gohr public function getIssued() 163455aa67eSAndreas Gohr { 164455aa67eSAndreas Gohr return $this->issued; 165455aa67eSAndreas Gohr } 166*403d6a9fSAndreas Gohr 167*403d6a9fSAndreas Gohr /** 168*403d6a9fSAndreas Gohr * Get the storage file for this token 169*403d6a9fSAndreas Gohr * 170*403d6a9fSAndreas Gohr * Tokens are stored to be able to invalidate them 171*403d6a9fSAndreas Gohr * 172*403d6a9fSAndreas Gohr * @param string $user The user the token is for 173*403d6a9fSAndreas Gohr * @return string 174*403d6a9fSAndreas Gohr */ 175*403d6a9fSAndreas Gohr public static function getStorageFile($user) 176*403d6a9fSAndreas Gohr { 177*403d6a9fSAndreas Gohr return getCacheName($user, '.token'); 178*403d6a9fSAndreas Gohr } 179455aa67eSAndreas Gohr} 180