You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

185 lines
4.3 KiB

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