xref: /dokuwiki/inc/JWT.php (revision 403d6a9f4cdd232c4e836e124374c49b4fcfcf08)
1455aa67eSAndreas Gohr<?php
2455aa67eSAndreas Gohr
3455aa67eSAndreas Gohrnamespace dokuwiki;
4455aa67eSAndreas Gohr
5455aa67eSAndreas Gohr/**
6455aa67eSAndreas Gohr * Minimal JWT implementation
7455aa67eSAndreas Gohr */
8455aa67eSAndreas Gohrclass JWT
9455aa67eSAndreas Gohr{
10455aa67eSAndreas Gohr
11455aa67eSAndreas Gohr    protected $user;
12455aa67eSAndreas Gohr    protected $issued;
13455aa67eSAndreas Gohr    protected $secret;
14455aa67eSAndreas Gohr
15455aa67eSAndreas Gohr    /**
16455aa67eSAndreas Gohr     * Create a new JWT object
17455aa67eSAndreas Gohr     *
18455aa67eSAndreas Gohr     * Use validate() or create() to create a new instance
19455aa67eSAndreas Gohr     *
20455aa67eSAndreas Gohr     * @param string $user
21455aa67eSAndreas Gohr     * @param int $issued
22455aa67eSAndreas Gohr     */
23455aa67eSAndreas Gohr    protected function __construct($user, $issued)
24455aa67eSAndreas Gohr    {
25455aa67eSAndreas Gohr        $this->user = $user;
26455aa67eSAndreas Gohr        $this->issued = $issued;
27455aa67eSAndreas Gohr    }
28455aa67eSAndreas Gohr
29455aa67eSAndreas Gohr    /**
30455aa67eSAndreas Gohr     * Load the cookiesalt as secret
31455aa67eSAndreas Gohr     *
32455aa67eSAndreas Gohr     * @return string
33455aa67eSAndreas Gohr     */
34455aa67eSAndreas Gohr    protected static function getSecret()
35455aa67eSAndreas Gohr    {
36455aa67eSAndreas Gohr        return auth_cookiesalt(false, true);
37455aa67eSAndreas Gohr    }
38455aa67eSAndreas Gohr
39455aa67eSAndreas Gohr    /**
40455aa67eSAndreas Gohr     * Create a new instance from a token
41455aa67eSAndreas Gohr     *
42455aa67eSAndreas Gohr     * @param $token
43455aa67eSAndreas Gohr     * @return self
44455aa67eSAndreas Gohr     * @throws \Exception
45455aa67eSAndreas Gohr     */
46455aa67eSAndreas Gohr    public static function validate($token)
47455aa67eSAndreas Gohr    {
48*403d6a9fSAndreas Gohr        [$header, $payload, $signature] = sexplode('.', $token, 3, '');
49455aa67eSAndreas Gohr        $signature = base64_decode($signature);
50455aa67eSAndreas Gohr
51455aa67eSAndreas Gohr        if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) {
52455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT signature');
53455aa67eSAndreas Gohr        }
54455aa67eSAndreas Gohr
55455aa67eSAndreas Gohr        $header = json_decode(base64_decode($header), true);
56455aa67eSAndreas Gohr        $payload = json_decode(base64_decode($payload), true);
57455aa67eSAndreas Gohr
58455aa67eSAndreas Gohr        if (!$header || !$payload || !$signature) {
59455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT');
60455aa67eSAndreas Gohr        }
61455aa67eSAndreas Gohr
62455aa67eSAndreas Gohr        if ($header['alg'] !== 'HS256') {
63455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT algorithm');
64455aa67eSAndreas Gohr        }
65455aa67eSAndreas Gohr        if ($header['typ'] !== 'JWT') {
66455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT type');
67455aa67eSAndreas Gohr        }
68455aa67eSAndreas Gohr        if ($payload['iss'] !== 'dokuwiki') {
69455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT issuer');
70455aa67eSAndreas Gohr        }
71455aa67eSAndreas Gohr        if (isset($payload['exp']) && $payload['exp'] < time()) {
72455aa67eSAndreas Gohr            throw new \Exception('JWT expired');
73455aa67eSAndreas Gohr        }
74455aa67eSAndreas Gohr
75455aa67eSAndreas Gohr        $user = $payload['sub'];
76*403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
77455aa67eSAndreas Gohr        if (!file_exists($file)) {
78455aa67eSAndreas Gohr            throw new \Exception('JWT not found, maybe it expired?');
79455aa67eSAndreas Gohr        }
80455aa67eSAndreas Gohr
81*403d6a9fSAndreas Gohr        if(file_get_contents($file) !== $token) {
82*403d6a9fSAndreas Gohr            throw new \Exception('JWT invalid, maybe it expired?');
83*403d6a9fSAndreas Gohr        }
84*403d6a9fSAndreas Gohr
85455aa67eSAndreas Gohr        return new self($user, $payload['iat']);
86455aa67eSAndreas Gohr    }
87455aa67eSAndreas Gohr
88455aa67eSAndreas Gohr    /**
89455aa67eSAndreas Gohr     * Create a new instance from a user
90455aa67eSAndreas Gohr     *
91455aa67eSAndreas Gohr     * Loads an existing token if available
92455aa67eSAndreas Gohr     *
93455aa67eSAndreas Gohr     * @param $user
94455aa67eSAndreas Gohr     * @return self
95455aa67eSAndreas Gohr     */
96455aa67eSAndreas Gohr    public static function fromUser($user)
97455aa67eSAndreas Gohr    {
98*403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
99455aa67eSAndreas Gohr
100455aa67eSAndreas Gohr        if (file_exists($file)) {
101455aa67eSAndreas Gohr            try {
102455aa67eSAndreas Gohr                return self::validate(io_readFile($file));
103455aa67eSAndreas Gohr            } catch (\Exception $ignored) {
104455aa67eSAndreas Gohr            }
105455aa67eSAndreas Gohr        }
106455aa67eSAndreas Gohr
107455aa67eSAndreas Gohr        $token = new self($user, time());
108455aa67eSAndreas Gohr        $token->save();
109455aa67eSAndreas Gohr        return $token;
110455aa67eSAndreas Gohr    }
111455aa67eSAndreas Gohr
112455aa67eSAndreas Gohr
113455aa67eSAndreas Gohr    /**
114455aa67eSAndreas Gohr     * Get the JWT token for this instance
115455aa67eSAndreas Gohr     *
116455aa67eSAndreas Gohr     * @return string
117455aa67eSAndreas Gohr     */
118455aa67eSAndreas Gohr    public function getToken()
119455aa67eSAndreas Gohr    {
120455aa67eSAndreas Gohr        $header = [
121455aa67eSAndreas Gohr            'alg' => 'HS256',
122455aa67eSAndreas Gohr            'typ' => 'JWT',
123455aa67eSAndreas Gohr        ];
124455aa67eSAndreas Gohr        $header = base64_encode(json_encode($header));
125455aa67eSAndreas Gohr        $payload = [
126455aa67eSAndreas Gohr            'iss' => 'dokuwiki',
127455aa67eSAndreas Gohr            'sub' => $this->user,
128455aa67eSAndreas Gohr            'iat' => $this->issued,
129455aa67eSAndreas Gohr        ];
130455aa67eSAndreas Gohr        $payload = base64_encode(json_encode($payload));
131455aa67eSAndreas Gohr        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
132455aa67eSAndreas Gohr        $signature = base64_encode($signature);
133455aa67eSAndreas Gohr        return "$header.$payload.$signature";
134455aa67eSAndreas Gohr    }
135455aa67eSAndreas Gohr
136455aa67eSAndreas Gohr    /**
137455aa67eSAndreas Gohr     * Save the token for the user
138455aa67eSAndreas Gohr     *
139455aa67eSAndreas Gohr     * Resets the issued timestamp
140455aa67eSAndreas Gohr     */
141455aa67eSAndreas Gohr    public function save()
142455aa67eSAndreas Gohr    {
143455aa67eSAndreas Gohr        $this->issued = time();
144*403d6a9fSAndreas Gohr        io_saveFile(self::getStorageFile($this->user), $this->getToken());
145455aa67eSAndreas Gohr    }
146455aa67eSAndreas Gohr
147455aa67eSAndreas Gohr    /**
148455aa67eSAndreas Gohr     * Get the user of this token
149455aa67eSAndreas Gohr     *
150455aa67eSAndreas Gohr     * @return string
151455aa67eSAndreas Gohr     */
152455aa67eSAndreas Gohr    public function getUser()
153455aa67eSAndreas Gohr    {
154455aa67eSAndreas Gohr        return $this->user;
155455aa67eSAndreas Gohr    }
156455aa67eSAndreas Gohr
157455aa67eSAndreas Gohr    /**
158455aa67eSAndreas Gohr     * Get the issued timestamp of this token
159455aa67eSAndreas Gohr     *
160455aa67eSAndreas Gohr     * @return int
161455aa67eSAndreas Gohr     */
162455aa67eSAndreas Gohr    public function getIssued()
163455aa67eSAndreas Gohr    {
164455aa67eSAndreas Gohr        return $this->issued;
165455aa67eSAndreas Gohr    }
166*403d6a9fSAndreas Gohr
167*403d6a9fSAndreas Gohr    /**
168*403d6a9fSAndreas Gohr     * Get the storage file for this token
169*403d6a9fSAndreas Gohr     *
170*403d6a9fSAndreas Gohr     * Tokens are stored to be able to invalidate them
171*403d6a9fSAndreas Gohr     *
172*403d6a9fSAndreas Gohr     * @param string $user The user the token is for
173*403d6a9fSAndreas Gohr     * @return string
174*403d6a9fSAndreas Gohr     */
175*403d6a9fSAndreas Gohr    public static function getStorageFile($user)
176*403d6a9fSAndreas Gohr    {
177*403d6a9fSAndreas Gohr        return getCacheName($user, '.token');
178*403d6a9fSAndreas Gohr    }
179455aa67eSAndreas Gohr}
180