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 $header = json_decode(base64_decode($header), true); 56 $payload = json_decode(base64_decode($payload), true); 57 58 if (!$header || !$payload || !$signature) { 59 throw new \Exception('Invalid JWT'); 60 } 61 62 if ($header['alg'] !== 'HS256') { 63 throw new \Exception('Unsupported JWT algorithm'); 64 } 65 if ($header['typ'] !== 'JWT') { 66 throw new \Exception('Unsupported JWT type'); 67 } 68 if ($payload['iss'] !== 'dokuwiki') { 69 throw new \Exception('Unsupported JWT issuer'); 70 } 71 if (isset($payload['exp']) && $payload['exp'] < time()) { 72 throw new \Exception('JWT expired'); 73 } 74 75 $user = $payload['sub']; 76 $file = self::getStorageFile($user); 77 if (!file_exists($file)) { 78 throw new \Exception('JWT not found, maybe it expired?'); 79 } 80 81 if(file_get_contents($file) !== $token) { 82 throw new \Exception('JWT invalid, maybe it expired?'); 83 } 84 85 return new self($user, $payload['iat']); 86 } 87 88 /** 89 * Create a new instance from a user 90 * 91 * Loads an existing token if available 92 * 93 * @param $user 94 * @return self 95 */ 96 public static function fromUser($user) 97 { 98 $file = self::getStorageFile($user); 99 100 if (file_exists($file)) { 101 try { 102 return self::validate(io_readFile($file)); 103 } catch (\Exception $ignored) { 104 } 105 } 106 107 $token = new self($user, time()); 108 $token->save(); 109 return $token; 110 } 111 112 113 /** 114 * Get the JWT token for this instance 115 * 116 * @return string 117 */ 118 public function getToken() 119 { 120 $header = [ 121 'alg' => 'HS256', 122 'typ' => 'JWT', 123 ]; 124 $header = base64_encode(json_encode($header)); 125 $payload = [ 126 'iss' => 'dokuwiki', 127 'sub' => $this->user, 128 'iat' => $this->issued, 129 ]; 130 $payload = base64_encode(json_encode($payload)); 131 $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 132 $signature = base64_encode($signature); 133 return "$header.$payload.$signature"; 134 } 135 136 /** 137 * Save the token for the user 138 * 139 * Resets the issued timestamp 140 */ 141 public function save() 142 { 143 $this->issued = time(); 144 io_saveFile(self::getStorageFile($this->user), $this->getToken()); 145 } 146 147 /** 148 * Get the user of this token 149 * 150 * @return string 151 */ 152 public function getUser() 153 { 154 return $this->user; 155 } 156 157 /** 158 * Get the issued timestamp of this token 159 * 160 * @return int 161 */ 162 public function getIssued() 163 { 164 return $this->issued; 165 } 166 167 /** 168 * Get the storage file for this token 169 * 170 * Tokens are stored to be able to invalidate them 171 * 172 * @param string $user The user the token is for 173 * @return string 174 */ 175 public static function getStorageFile($user) 176 { 177 return getCacheName($user, '.token'); 178 } 179} 180