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