xref: /dokuwiki/inc/JWT.php (revision 4a9d6ae2968b05434349b05b73ef93f5836ab0f8)
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    protected $user;
11455aa67eSAndreas Gohr    protected $issued;
12455aa67eSAndreas Gohr    protected $secret;
13455aa67eSAndreas Gohr
14455aa67eSAndreas Gohr    /**
15455aa67eSAndreas Gohr     * Create a new JWT object
16455aa67eSAndreas Gohr     *
17455aa67eSAndreas Gohr     * Use validate() or create() to create a new instance
18455aa67eSAndreas Gohr     *
19455aa67eSAndreas Gohr     * @param string $user
20455aa67eSAndreas Gohr     * @param int $issued
21455aa67eSAndreas Gohr     */
22455aa67eSAndreas Gohr    protected function __construct($user, $issued)
23455aa67eSAndreas Gohr    {
24455aa67eSAndreas Gohr        $this->user = $user;
25455aa67eSAndreas Gohr        $this->issued = $issued;
26455aa67eSAndreas Gohr    }
27455aa67eSAndreas Gohr
28455aa67eSAndreas Gohr    /**
29455aa67eSAndreas Gohr     * Load the cookiesalt as secret
30455aa67eSAndreas Gohr     *
31455aa67eSAndreas Gohr     * @return string
32455aa67eSAndreas Gohr     */
33455aa67eSAndreas Gohr    protected static function getSecret()
34455aa67eSAndreas Gohr    {
35455aa67eSAndreas Gohr        return auth_cookiesalt(false, true);
36455aa67eSAndreas Gohr    }
37455aa67eSAndreas Gohr
38455aa67eSAndreas Gohr    /**
39455aa67eSAndreas Gohr     * Create a new instance from a token
40455aa67eSAndreas Gohr     *
41455aa67eSAndreas Gohr     * @param $token
42455aa67eSAndreas Gohr     * @return self
43455aa67eSAndreas Gohr     * @throws \Exception
44455aa67eSAndreas Gohr     */
45455aa67eSAndreas Gohr    public static function validate($token)
46455aa67eSAndreas Gohr    {
47403d6a9fSAndreas Gohr        [$header, $payload, $signature] = sexplode('.', $token, 3, '');
48455aa67eSAndreas Gohr        $signature = base64_decode($signature);
49455aa67eSAndreas Gohr
50455aa67eSAndreas Gohr        if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) {
51455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT signature');
52455aa67eSAndreas Gohr        }
53455aa67eSAndreas Gohr
5487603a0aSAndreas Gohr        try {
5587603a0aSAndreas Gohr            $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR);
5687603a0aSAndreas Gohr            $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR);
5787603a0aSAndreas Gohr        } catch (\Exception $e) {
58cf927d07Ssplitbrain            throw new \Exception('Invalid JWT', $e->getCode(), $e);
5987603a0aSAndreas Gohr        }
60455aa67eSAndreas Gohr
61455aa67eSAndreas Gohr        if (!$header || !$payload || !$signature) {
62455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT');
63455aa67eSAndreas Gohr        }
64455aa67eSAndreas Gohr
65455aa67eSAndreas Gohr        if ($header['alg'] !== 'HS256') {
66455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT algorithm');
67455aa67eSAndreas Gohr        }
68455aa67eSAndreas Gohr        if ($header['typ'] !== 'JWT') {
69455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT type');
70455aa67eSAndreas Gohr        }
71455aa67eSAndreas Gohr        if ($payload['iss'] !== 'dokuwiki') {
72455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT issuer');
73455aa67eSAndreas Gohr        }
74455aa67eSAndreas Gohr        if (isset($payload['exp']) && $payload['exp'] < time()) {
75455aa67eSAndreas Gohr            throw new \Exception('JWT expired');
76455aa67eSAndreas Gohr        }
77455aa67eSAndreas Gohr
78455aa67eSAndreas Gohr        $user = $payload['sub'];
79403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
80455aa67eSAndreas Gohr        if (!file_exists($file)) {
81455aa67eSAndreas Gohr            throw new \Exception('JWT not found, maybe it expired?');
82455aa67eSAndreas Gohr        }
83455aa67eSAndreas Gohr
84403d6a9fSAndreas Gohr        if (file_get_contents($file) !== $token) {
85403d6a9fSAndreas Gohr            throw new \Exception('JWT invalid, maybe it expired?');
86403d6a9fSAndreas Gohr        }
87403d6a9fSAndreas Gohr
88455aa67eSAndreas Gohr        return new self($user, $payload['iat']);
89455aa67eSAndreas Gohr    }
90455aa67eSAndreas Gohr
91455aa67eSAndreas Gohr    /**
92455aa67eSAndreas Gohr     * Create a new instance from a user
93455aa67eSAndreas Gohr     *
94455aa67eSAndreas Gohr     * Loads an existing token if available
95455aa67eSAndreas Gohr     *
96455aa67eSAndreas Gohr     * @param $user
97455aa67eSAndreas Gohr     * @return self
98455aa67eSAndreas Gohr     */
99455aa67eSAndreas Gohr    public static function fromUser($user)
100455aa67eSAndreas Gohr    {
101403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
102455aa67eSAndreas Gohr
103455aa67eSAndreas Gohr        if (file_exists($file)) {
104455aa67eSAndreas Gohr            try {
105455aa67eSAndreas Gohr                return self::validate(io_readFile($file));
106455aa67eSAndreas Gohr            } catch (\Exception $ignored) {
107455aa67eSAndreas Gohr            }
108455aa67eSAndreas Gohr        }
109455aa67eSAndreas Gohr
110455aa67eSAndreas Gohr        $token = new self($user, time());
111455aa67eSAndreas Gohr        $token->save();
112455aa67eSAndreas Gohr        return $token;
113455aa67eSAndreas Gohr    }
114455aa67eSAndreas Gohr
115455aa67eSAndreas Gohr
116455aa67eSAndreas Gohr    /**
117455aa67eSAndreas Gohr     * Get the JWT token for this instance
118455aa67eSAndreas Gohr     *
119455aa67eSAndreas Gohr     * @return string
120455aa67eSAndreas Gohr     */
121455aa67eSAndreas Gohr    public function getToken()
122455aa67eSAndreas Gohr    {
123455aa67eSAndreas Gohr        $header = [
124455aa67eSAndreas Gohr            'alg' => 'HS256',
125455aa67eSAndreas Gohr            'typ' => 'JWT',
126455aa67eSAndreas Gohr        ];
127455aa67eSAndreas Gohr        $header = base64_encode(json_encode($header));
128cf927d07Ssplitbrain
129455aa67eSAndreas Gohr        $payload = [
130455aa67eSAndreas Gohr            'iss' => 'dokuwiki',
131455aa67eSAndreas Gohr            'sub' => $this->user,
132455aa67eSAndreas Gohr            'iat' => $this->issued,
133455aa67eSAndreas Gohr        ];
134cf927d07Ssplitbrain        $payload = base64_encode(json_encode($payload, JSON_THROW_ON_ERROR));
135cf927d07Ssplitbrain
136455aa67eSAndreas Gohr        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
137455aa67eSAndreas Gohr        $signature = base64_encode($signature);
138455aa67eSAndreas Gohr        return "$header.$payload.$signature";
139455aa67eSAndreas Gohr    }
140455aa67eSAndreas Gohr
141455aa67eSAndreas Gohr    /**
142455aa67eSAndreas Gohr     * Save the token for the user
143455aa67eSAndreas Gohr     *
144455aa67eSAndreas Gohr     * Resets the issued timestamp
145455aa67eSAndreas Gohr     */
146455aa67eSAndreas Gohr    public function save()
147455aa67eSAndreas Gohr    {
148455aa67eSAndreas Gohr        $this->issued = time();
149403d6a9fSAndreas Gohr        io_saveFile(self::getStorageFile($this->user), $this->getToken());
150455aa67eSAndreas Gohr    }
151455aa67eSAndreas Gohr
152455aa67eSAndreas Gohr    /**
153455aa67eSAndreas Gohr     * Get the user of this token
154455aa67eSAndreas Gohr     *
155455aa67eSAndreas Gohr     * @return string
156455aa67eSAndreas Gohr     */
157455aa67eSAndreas Gohr    public function getUser()
158455aa67eSAndreas Gohr    {
159455aa67eSAndreas Gohr        return $this->user;
160455aa67eSAndreas Gohr    }
161455aa67eSAndreas Gohr
162455aa67eSAndreas Gohr    /**
163455aa67eSAndreas Gohr     * Get the issued timestamp of this token
164455aa67eSAndreas Gohr     *
165455aa67eSAndreas Gohr     * @return int
166455aa67eSAndreas Gohr     */
167455aa67eSAndreas Gohr    public function getIssued()
168455aa67eSAndreas Gohr    {
169455aa67eSAndreas Gohr        return $this->issued;
170455aa67eSAndreas Gohr    }
171403d6a9fSAndreas Gohr
172403d6a9fSAndreas Gohr    /**
173403d6a9fSAndreas Gohr     * Get the storage file for this token
174403d6a9fSAndreas Gohr     *
175403d6a9fSAndreas Gohr     * Tokens are stored to be able to invalidate them
176403d6a9fSAndreas Gohr     *
177403d6a9fSAndreas Gohr     * @param string $user The user the token is for
178403d6a9fSAndreas Gohr     * @return string
179403d6a9fSAndreas Gohr     */
180403d6a9fSAndreas Gohr    public static function getStorageFile($user)
181403d6a9fSAndreas Gohr    {
182*4a9d6ae2Smauli        global $conf;
183*4a9d6ae2Smauli        $hash = hash('sha256', $user);
184*4a9d6ae2Smauli        $file = $conf['metadir'] . '/jwt/' . $hash[0] . '/' . $hash . '.token';
185*4a9d6ae2Smauli        io_makeFileDir($file);
186*4a9d6ae2Smauli        return $file;
187403d6a9fSAndreas Gohr    }
188455aa67eSAndreas Gohr}
189