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