xref: /dokuwiki/inc/JWT.php (revision 455aa67e850e236be8cd442e32eec2b8fff15fb2)
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 = getCacheName($user, '.token');
77        if (!file_exists($file)) {
78            throw new \Exception('JWT not found, maybe it expired?');
79        }
80
81        return new self($user, $payload['iat']);
82    }
83
84    /**
85     * Create a new instance from a user
86     *
87     * Loads an existing token if available
88     *
89     * @param $user
90     * @return self
91     */
92    public static function fromUser($user)
93    {
94        $file = getCacheName($user, '.token');
95
96        if (file_exists($file)) {
97            try {
98                return self::validate(io_readFile($file));
99            } catch (\Exception $ignored) {
100            }
101        }
102
103        $token = new self($user, time());
104        $token->save();
105        return $token;
106    }
107
108
109    /**
110     * Get the JWT token for this instance
111     *
112     * @return string
113     */
114    public function getToken()
115    {
116        $header = [
117            'alg' => 'HS256',
118            'typ' => 'JWT',
119        ];
120        $header = base64_encode(json_encode($header));
121        $payload = [
122            'iss' => 'dokuwiki',
123            'sub' => $this->user,
124            'iat' => $this->issued,
125        ];
126        $payload = base64_encode(json_encode($payload));
127        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
128        $signature = base64_encode($signature);
129        return "$header.$payload.$signature";
130    }
131
132    /**
133     * Save the token for the user
134     *
135     * Resets the issued timestamp
136     */
137    public function save()
138    {
139        $this->issued = time();
140        $file = getCacheName($this->user, '.token');
141        io_saveFile($file, $this->getToken());
142    }
143
144    /**
145     * Get the user of this token
146     *
147     * @return string
148     */
149    public function getUser()
150    {
151        return $this->user;
152    }
153
154    /**
155     * Get the issued timestamp of this token
156     *
157     * @return int
158     */
159    public function getIssued()
160    {
161        return $this->issued;
162    }
163}
164