1 <?php
2 
3 namespace dokuwiki;
4 
5 /**
6  * Minimal JWT implementation
7  */
8 class 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