xref: /dokuwiki/inc/JWT.php (revision 6f8e03f5bc790d26e1215349cea98d5a73654139)
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        try {
56            $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR);
57            $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR);
58        } catch (\Exception $e) {
59            throw new \Exception('Invalid JWT');
60        }
61
62        if (!$header || !$payload || !$signature) {
63            throw new \Exception('Invalid JWT');
64        }
65
66        if ($header['alg'] !== 'HS256') {
67            throw new \Exception('Unsupported JWT algorithm');
68        }
69        if ($header['typ'] !== 'JWT') {
70            throw new \Exception('Unsupported JWT type');
71        }
72        if ($payload['iss'] !== 'dokuwiki') {
73            throw new \Exception('Unsupported JWT issuer');
74        }
75        if (isset($payload['exp']) && $payload['exp'] < time()) {
76            throw new \Exception('JWT expired');
77        }
78
79        $user = $payload['sub'];
80        $file = self::getStorageFile($user);
81        if (!file_exists($file)) {
82            throw new \Exception('JWT not found, maybe it expired?');
83        }
84
85        if(file_get_contents($file) !== $token) {
86            throw new \Exception('JWT invalid, maybe it expired?');
87        }
88
89        return new self($user, $payload['iat']);
90    }
91
92    /**
93     * Create a new instance from a user
94     *
95     * Loads an existing token if available
96     *
97     * @param $user
98     * @return self
99     */
100    public static function fromUser($user)
101    {
102        $file = self::getStorageFile($user);
103
104        if (file_exists($file)) {
105            try {
106                return self::validate(io_readFile($file));
107            } catch (\Exception $ignored) {
108            }
109        }
110
111        $token = new self($user, time());
112        $token->save();
113        return $token;
114    }
115
116
117    /**
118     * Get the JWT token for this instance
119     *
120     * @return string
121     */
122    public function getToken()
123    {
124        $header = [
125            'alg' => 'HS256',
126            'typ' => 'JWT',
127        ];
128        $header = base64_encode(json_encode($header));
129        $payload = [
130            'iss' => 'dokuwiki',
131            'sub' => $this->user,
132            'iat' => $this->issued,
133        ];
134        $payload = base64_encode(json_encode($payload));
135        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
136        $signature = base64_encode($signature);
137        return "$header.$payload.$signature";
138    }
139
140    /**
141     * Save the token for the user
142     *
143     * Resets the issued timestamp
144     */
145    public function save()
146    {
147        $this->issued = time();
148        io_saveFile(self::getStorageFile($this->user), $this->getToken());
149    }
150
151    /**
152     * Get the user of this token
153     *
154     * @return string
155     */
156    public function getUser()
157    {
158        return $this->user;
159    }
160
161    /**
162     * Get the issued timestamp of this token
163     *
164     * @return int
165     */
166    public function getIssued()
167    {
168        return $this->issued;
169    }
170
171    /**
172     * Get the storage file for this token
173     *
174     * Tokens are stored to be able to invalidate them
175     *
176     * @param string $user The user the token is for
177     * @return string
178     */
179    public static function getStorageFile($user)
180    {
181        return getCacheName($user, '.token');
182    }
183}
184