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 protected $user; 11455aa67eSAndreas Gohr protected $issued; 12455aa67eSAndreas Gohr protected $secret; 13455aa67eSAndreas Gohr 14455aa67eSAndreas Gohr /** 15455aa67eSAndreas Gohr * Create a new JWT object 16455aa67eSAndreas Gohr * 17455aa67eSAndreas Gohr * Use validate() or create() to create a new instance 18455aa67eSAndreas Gohr * 19455aa67eSAndreas Gohr * @param string $user 20455aa67eSAndreas Gohr * @param int $issued 21455aa67eSAndreas Gohr */ 22455aa67eSAndreas Gohr protected function __construct($user, $issued) 23455aa67eSAndreas Gohr { 24455aa67eSAndreas Gohr $this->user = $user; 25455aa67eSAndreas Gohr $this->issued = $issued; 26455aa67eSAndreas Gohr } 27455aa67eSAndreas Gohr 28455aa67eSAndreas Gohr /** 29455aa67eSAndreas Gohr * Load the cookiesalt as secret 30455aa67eSAndreas Gohr * 31455aa67eSAndreas Gohr * @return string 32455aa67eSAndreas Gohr */ 33455aa67eSAndreas Gohr protected static function getSecret() 34455aa67eSAndreas Gohr { 35455aa67eSAndreas Gohr return auth_cookiesalt(false, true); 36455aa67eSAndreas Gohr } 37455aa67eSAndreas Gohr 38455aa67eSAndreas Gohr /** 39455aa67eSAndreas Gohr * Create a new instance from a token 40455aa67eSAndreas Gohr * 41455aa67eSAndreas Gohr * @param $token 42455aa67eSAndreas Gohr * @return self 43455aa67eSAndreas Gohr * @throws \Exception 44455aa67eSAndreas Gohr */ 45455aa67eSAndreas Gohr public static function validate($token) 46455aa67eSAndreas Gohr { 47403d6a9fSAndreas Gohr [$header, $payload, $signature] = sexplode('.', $token, 3, ''); 48455aa67eSAndreas Gohr $signature = base64_decode($signature); 49455aa67eSAndreas Gohr 50455aa67eSAndreas Gohr if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) { 51455aa67eSAndreas Gohr throw new \Exception('Invalid JWT signature'); 52455aa67eSAndreas Gohr } 53455aa67eSAndreas Gohr 5487603a0aSAndreas Gohr try { 5587603a0aSAndreas Gohr $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR); 5687603a0aSAndreas Gohr $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR); 5787603a0aSAndreas Gohr } catch (\Exception $e) { 58cf927d07Ssplitbrain throw new \Exception('Invalid JWT', $e->getCode(), $e); 5987603a0aSAndreas Gohr } 60455aa67eSAndreas Gohr 61455aa67eSAndreas Gohr if (!$header || !$payload || !$signature) { 62455aa67eSAndreas Gohr throw new \Exception('Invalid JWT'); 63455aa67eSAndreas Gohr } 64455aa67eSAndreas Gohr 65455aa67eSAndreas Gohr if ($header['alg'] !== 'HS256') { 66455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT algorithm'); 67455aa67eSAndreas Gohr } 68455aa67eSAndreas Gohr if ($header['typ'] !== 'JWT') { 69455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT type'); 70455aa67eSAndreas Gohr } 71455aa67eSAndreas Gohr if ($payload['iss'] !== 'dokuwiki') { 72455aa67eSAndreas Gohr throw new \Exception('Unsupported JWT issuer'); 73455aa67eSAndreas Gohr } 74455aa67eSAndreas Gohr if (isset($payload['exp']) && $payload['exp'] < time()) { 75455aa67eSAndreas Gohr throw new \Exception('JWT expired'); 76455aa67eSAndreas Gohr } 77455aa67eSAndreas Gohr 78455aa67eSAndreas Gohr $user = $payload['sub']; 79403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 80455aa67eSAndreas Gohr if (!file_exists($file)) { 81455aa67eSAndreas Gohr throw new \Exception('JWT not found, maybe it expired?'); 82455aa67eSAndreas Gohr } 83455aa67eSAndreas Gohr 84403d6a9fSAndreas Gohr if (file_get_contents($file) !== $token) { 85403d6a9fSAndreas Gohr throw new \Exception('JWT invalid, maybe it expired?'); 86403d6a9fSAndreas Gohr } 87403d6a9fSAndreas Gohr 88455aa67eSAndreas Gohr return new self($user, $payload['iat']); 89455aa67eSAndreas Gohr } 90455aa67eSAndreas Gohr 91455aa67eSAndreas Gohr /** 92455aa67eSAndreas Gohr * Create a new instance from a user 93455aa67eSAndreas Gohr * 94455aa67eSAndreas Gohr * Loads an existing token if available 95455aa67eSAndreas Gohr * 96455aa67eSAndreas Gohr * @param $user 97455aa67eSAndreas Gohr * @return self 98455aa67eSAndreas Gohr */ 99455aa67eSAndreas Gohr public static function fromUser($user) 100455aa67eSAndreas Gohr { 101403d6a9fSAndreas Gohr $file = self::getStorageFile($user); 102455aa67eSAndreas Gohr 103455aa67eSAndreas Gohr if (file_exists($file)) { 104455aa67eSAndreas Gohr try { 105455aa67eSAndreas Gohr return self::validate(io_readFile($file)); 106455aa67eSAndreas Gohr } catch (\Exception $ignored) { 107455aa67eSAndreas Gohr } 108455aa67eSAndreas Gohr } 109455aa67eSAndreas Gohr 110455aa67eSAndreas Gohr $token = new self($user, time()); 111455aa67eSAndreas Gohr $token->save(); 112455aa67eSAndreas Gohr return $token; 113455aa67eSAndreas Gohr } 114455aa67eSAndreas Gohr 115455aa67eSAndreas Gohr 116455aa67eSAndreas Gohr /** 117455aa67eSAndreas Gohr * Get the JWT token for this instance 118455aa67eSAndreas Gohr * 119455aa67eSAndreas Gohr * @return string 120455aa67eSAndreas Gohr */ 121455aa67eSAndreas Gohr public function getToken() 122455aa67eSAndreas Gohr { 123455aa67eSAndreas Gohr $header = [ 124455aa67eSAndreas Gohr 'alg' => 'HS256', 125455aa67eSAndreas Gohr 'typ' => 'JWT', 126455aa67eSAndreas Gohr ]; 127455aa67eSAndreas Gohr $header = base64_encode(json_encode($header)); 128cf927d07Ssplitbrain 129455aa67eSAndreas Gohr $payload = [ 130455aa67eSAndreas Gohr 'iss' => 'dokuwiki', 131455aa67eSAndreas Gohr 'sub' => $this->user, 132455aa67eSAndreas Gohr 'iat' => $this->issued, 133455aa67eSAndreas Gohr ]; 134cf927d07Ssplitbrain $payload = base64_encode(json_encode($payload, JSON_THROW_ON_ERROR)); 135cf927d07Ssplitbrain 136455aa67eSAndreas Gohr $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 137455aa67eSAndreas Gohr $signature = base64_encode($signature); 138455aa67eSAndreas Gohr return "$header.$payload.$signature"; 139455aa67eSAndreas Gohr } 140455aa67eSAndreas Gohr 141455aa67eSAndreas Gohr /** 142455aa67eSAndreas Gohr * Save the token for the user 143455aa67eSAndreas Gohr * 144455aa67eSAndreas Gohr * Resets the issued timestamp 145455aa67eSAndreas Gohr */ 146455aa67eSAndreas Gohr public function save() 147455aa67eSAndreas Gohr { 148455aa67eSAndreas Gohr $this->issued = time(); 149403d6a9fSAndreas Gohr io_saveFile(self::getStorageFile($this->user), $this->getToken()); 150455aa67eSAndreas Gohr } 151455aa67eSAndreas Gohr 152455aa67eSAndreas Gohr /** 153455aa67eSAndreas Gohr * Get the user of this token 154455aa67eSAndreas Gohr * 155455aa67eSAndreas Gohr * @return string 156455aa67eSAndreas Gohr */ 157455aa67eSAndreas Gohr public function getUser() 158455aa67eSAndreas Gohr { 159455aa67eSAndreas Gohr return $this->user; 160455aa67eSAndreas Gohr } 161455aa67eSAndreas Gohr 162455aa67eSAndreas Gohr /** 163455aa67eSAndreas Gohr * Get the issued timestamp of this token 164455aa67eSAndreas Gohr * 165455aa67eSAndreas Gohr * @return int 166455aa67eSAndreas Gohr */ 167455aa67eSAndreas Gohr public function getIssued() 168455aa67eSAndreas Gohr { 169455aa67eSAndreas Gohr return $this->issued; 170455aa67eSAndreas Gohr } 171403d6a9fSAndreas Gohr 172403d6a9fSAndreas Gohr /** 173403d6a9fSAndreas Gohr * Get the storage file for this token 174403d6a9fSAndreas Gohr * 175403d6a9fSAndreas Gohr * Tokens are stored to be able to invalidate them 176403d6a9fSAndreas Gohr * 177403d6a9fSAndreas Gohr * @param string $user The user the token is for 178403d6a9fSAndreas Gohr * @return string 179403d6a9fSAndreas Gohr */ 180403d6a9fSAndreas Gohr public static function getStorageFile($user) 181403d6a9fSAndreas Gohr { 182*4a9d6ae2Smauli global $conf; 183*4a9d6ae2Smauli $hash = hash('sha256', $user); 184*4a9d6ae2Smauli $file = $conf['metadir'] . '/jwt/' . $hash[0] . '/' . $hash . '.token'; 185*4a9d6ae2Smauli io_makeFileDir($file); 186*4a9d6ae2Smauli return $file; 187403d6a9fSAndreas Gohr } 188455aa67eSAndreas Gohr} 189