1<?php
2
3namespace dokuwiki;
4
5/**
6 * Minimal JWT implementation
7 */
8class 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        return getCacheName($user, '.token');
183    }
184}
185