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