1<?php 2 3namespace dokuwiki; 4 5/** 6 * Minimal JWT implementation 7 */ 8class JWT 9{ 10 11 protected $user; 12 protected $issued; 13 protected $secret; 14 15 /** 16 * Create a new JWT object 17 * 18 * Use validate() or create() to create a new instance 19 * 20 * @param string $user 21 * @param int $issued 22 */ 23 protected function __construct($user, $issued) 24 { 25 $this->user = $user; 26 $this->issued = $issued; 27 } 28 29 /** 30 * Load the cookiesalt as secret 31 * 32 * @return string 33 */ 34 protected static function getSecret() 35 { 36 return auth_cookiesalt(false, true); 37 } 38 39 /** 40 * Create a new instance from a token 41 * 42 * @param $token 43 * @return self 44 * @throws \Exception 45 */ 46 public static function validate($token) 47 { 48 [$header, $payload, $signature] = sexplode('.', $token, 3, ''); 49 $signature = base64_decode($signature); 50 51 if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) { 52 throw new \Exception('Invalid JWT signature'); 53 } 54 55 try { 56 $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR); 57 $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR); 58 } catch (\Exception $e) { 59 throw new \Exception('Invalid JWT'); 60 } 61 62 if (!$header || !$payload || !$signature) { 63 throw new \Exception('Invalid JWT'); 64 } 65 66 if ($header['alg'] !== 'HS256') { 67 throw new \Exception('Unsupported JWT algorithm'); 68 } 69 if ($header['typ'] !== 'JWT') { 70 throw new \Exception('Unsupported JWT type'); 71 } 72 if ($payload['iss'] !== 'dokuwiki') { 73 throw new \Exception('Unsupported JWT issuer'); 74 } 75 if (isset($payload['exp']) && $payload['exp'] < time()) { 76 throw new \Exception('JWT expired'); 77 } 78 79 $user = $payload['sub']; 80 $file = self::getStorageFile($user); 81 if (!file_exists($file)) { 82 throw new \Exception('JWT not found, maybe it expired?'); 83 } 84 85 if(file_get_contents($file) !== $token) { 86 throw new \Exception('JWT invalid, maybe it expired?'); 87 } 88 89 return new self($user, $payload['iat']); 90 } 91 92 /** 93 * Create a new instance from a user 94 * 95 * Loads an existing token if available 96 * 97 * @param $user 98 * @return self 99 */ 100 public static function fromUser($user) 101 { 102 $file = self::getStorageFile($user); 103 104 if (file_exists($file)) { 105 try { 106 return self::validate(io_readFile($file)); 107 } catch (\Exception $ignored) { 108 } 109 } 110 111 $token = new self($user, time()); 112 $token->save(); 113 return $token; 114 } 115 116 117 /** 118 * Get the JWT token for this instance 119 * 120 * @return string 121 */ 122 public function getToken() 123 { 124 $header = [ 125 'alg' => 'HS256', 126 'typ' => 'JWT', 127 ]; 128 $header = base64_encode(json_encode($header)); 129 $payload = [ 130 'iss' => 'dokuwiki', 131 'sub' => $this->user, 132 'iat' => $this->issued, 133 ]; 134 $payload = base64_encode(json_encode($payload)); 135 $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 136 $signature = base64_encode($signature); 137 return "$header.$payload.$signature"; 138 } 139 140 /** 141 * Save the token for the user 142 * 143 * Resets the issued timestamp 144 */ 145 public function save() 146 { 147 $this->issued = time(); 148 io_saveFile(self::getStorageFile($this->user), $this->getToken()); 149 } 150 151 /** 152 * Get the user of this token 153 * 154 * @return string 155 */ 156 public function getUser() 157 { 158 return $this->user; 159 } 160 161 /** 162 * Get the issued timestamp of this token 163 * 164 * @return int 165 */ 166 public function getIssued() 167 { 168 return $this->issued; 169 } 170 171 /** 172 * Get the storage file for this token 173 * 174 * Tokens are stored to be able to invalidate them 175 * 176 * @param string $user The user the token is for 177 * @return string 178 */ 179 public static function getStorageFile($user) 180 { 181 return getCacheName($user, '.token'); 182 } 183} 184