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 = getCacheName($user, '.token'); 77 if (!file_exists($file)) { 78 throw new \Exception('JWT not found, maybe it expired?'); 79 } 80 81 return new self($user, $payload['iat']); 82 } 83 84 /** 85 * Create a new instance from a user 86 * 87 * Loads an existing token if available 88 * 89 * @param $user 90 * @return self 91 */ 92 public static function fromUser($user) 93 { 94 $file = getCacheName($user, '.token'); 95 96 if (file_exists($file)) { 97 try { 98 return self::validate(io_readFile($file)); 99 } catch (\Exception $ignored) { 100 } 101 } 102 103 $token = new self($user, time()); 104 $token->save(); 105 return $token; 106 } 107 108 109 /** 110 * Get the JWT token for this instance 111 * 112 * @return string 113 */ 114 public function getToken() 115 { 116 $header = [ 117 'alg' => 'HS256', 118 'typ' => 'JWT', 119 ]; 120 $header = base64_encode(json_encode($header)); 121 $payload = [ 122 'iss' => 'dokuwiki', 123 'sub' => $this->user, 124 'iat' => $this->issued, 125 ]; 126 $payload = base64_encode(json_encode($payload)); 127 $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true); 128 $signature = base64_encode($signature); 129 return "$header.$payload.$signature"; 130 } 131 132 /** 133 * Save the token for the user 134 * 135 * Resets the issued timestamp 136 */ 137 public function save() 138 { 139 $this->issued = time(); 140 $file = getCacheName($this->user, '.token'); 141 io_saveFile($file, $this->getToken()); 142 } 143 144 /** 145 * Get the user of this token 146 * 147 * @return string 148 */ 149 public function getUser() 150 { 151 return $this->user; 152 } 153 154 /** 155 * Get the issued timestamp of this token 156 * 157 * @return int 158 */ 159 public function getIssued() 160 { 161 return $this->issued; 162 } 163} 164