xref: /dokuwiki/inc/JWT.php (revision 87603a0ae6bb5f9492d204f5b6cdd611ac4bc13a)
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    {
48403d6a9fSAndreas 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
55*87603a0aSAndreas Gohr        try {
56*87603a0aSAndreas Gohr            $header = json_decode(base64_decode($header), true, 512, JSON_THROW_ON_ERROR);
57*87603a0aSAndreas Gohr            $payload = json_decode(base64_decode($payload), true, 512, JSON_THROW_ON_ERROR);
58*87603a0aSAndreas Gohr        } catch (\Exception $e) {
59*87603a0aSAndreas Gohr            throw new \Exception('Invalid JWT');
60*87603a0aSAndreas Gohr        }
61455aa67eSAndreas Gohr
62455aa67eSAndreas Gohr        if (!$header || !$payload || !$signature) {
63455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT');
64455aa67eSAndreas Gohr        }
65455aa67eSAndreas Gohr
66455aa67eSAndreas Gohr        if ($header['alg'] !== 'HS256') {
67455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT algorithm');
68455aa67eSAndreas Gohr        }
69455aa67eSAndreas Gohr        if ($header['typ'] !== 'JWT') {
70455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT type');
71455aa67eSAndreas Gohr        }
72455aa67eSAndreas Gohr        if ($payload['iss'] !== 'dokuwiki') {
73455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT issuer');
74455aa67eSAndreas Gohr        }
75455aa67eSAndreas Gohr        if (isset($payload['exp']) && $payload['exp'] < time()) {
76455aa67eSAndreas Gohr            throw new \Exception('JWT expired');
77455aa67eSAndreas Gohr        }
78455aa67eSAndreas Gohr
79455aa67eSAndreas Gohr        $user = $payload['sub'];
80403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
81455aa67eSAndreas Gohr        if (!file_exists($file)) {
82455aa67eSAndreas Gohr            throw new \Exception('JWT not found, maybe it expired?');
83455aa67eSAndreas Gohr        }
84455aa67eSAndreas Gohr
85403d6a9fSAndreas Gohr        if(file_get_contents($file) !== $token) {
86403d6a9fSAndreas Gohr            throw new \Exception('JWT invalid, maybe it expired?');
87403d6a9fSAndreas Gohr        }
88403d6a9fSAndreas Gohr
89455aa67eSAndreas Gohr        return new self($user, $payload['iat']);
90455aa67eSAndreas Gohr    }
91455aa67eSAndreas Gohr
92455aa67eSAndreas Gohr    /**
93455aa67eSAndreas Gohr     * Create a new instance from a user
94455aa67eSAndreas Gohr     *
95455aa67eSAndreas Gohr     * Loads an existing token if available
96455aa67eSAndreas Gohr     *
97455aa67eSAndreas Gohr     * @param $user
98455aa67eSAndreas Gohr     * @return self
99455aa67eSAndreas Gohr     */
100455aa67eSAndreas Gohr    public static function fromUser($user)
101455aa67eSAndreas Gohr    {
102403d6a9fSAndreas Gohr        $file = self::getStorageFile($user);
103455aa67eSAndreas Gohr
104455aa67eSAndreas Gohr        if (file_exists($file)) {
105455aa67eSAndreas Gohr            try {
106455aa67eSAndreas Gohr                return self::validate(io_readFile($file));
107455aa67eSAndreas Gohr            } catch (\Exception $ignored) {
108455aa67eSAndreas Gohr            }
109455aa67eSAndreas Gohr        }
110455aa67eSAndreas Gohr
111455aa67eSAndreas Gohr        $token = new self($user, time());
112455aa67eSAndreas Gohr        $token->save();
113455aa67eSAndreas Gohr        return $token;
114455aa67eSAndreas Gohr    }
115455aa67eSAndreas Gohr
116455aa67eSAndreas Gohr
117455aa67eSAndreas Gohr    /**
118455aa67eSAndreas Gohr     * Get the JWT token for this instance
119455aa67eSAndreas Gohr     *
120455aa67eSAndreas Gohr     * @return string
121455aa67eSAndreas Gohr     */
122455aa67eSAndreas Gohr    public function getToken()
123455aa67eSAndreas Gohr    {
124455aa67eSAndreas Gohr        $header = [
125455aa67eSAndreas Gohr            'alg' => 'HS256',
126455aa67eSAndreas Gohr            'typ' => 'JWT',
127455aa67eSAndreas Gohr        ];
128455aa67eSAndreas Gohr        $header = base64_encode(json_encode($header));
129455aa67eSAndreas Gohr        $payload = [
130455aa67eSAndreas Gohr            'iss' => 'dokuwiki',
131455aa67eSAndreas Gohr            'sub' => $this->user,
132455aa67eSAndreas Gohr            'iat' => $this->issued,
133455aa67eSAndreas Gohr        ];
134455aa67eSAndreas Gohr        $payload = base64_encode(json_encode($payload));
135455aa67eSAndreas Gohr        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
136455aa67eSAndreas Gohr        $signature = base64_encode($signature);
137455aa67eSAndreas Gohr        return "$header.$payload.$signature";
138455aa67eSAndreas Gohr    }
139455aa67eSAndreas Gohr
140455aa67eSAndreas Gohr    /**
141455aa67eSAndreas Gohr     * Save the token for the user
142455aa67eSAndreas Gohr     *
143455aa67eSAndreas Gohr     * Resets the issued timestamp
144455aa67eSAndreas Gohr     */
145455aa67eSAndreas Gohr    public function save()
146455aa67eSAndreas Gohr    {
147455aa67eSAndreas Gohr        $this->issued = time();
148403d6a9fSAndreas Gohr        io_saveFile(self::getStorageFile($this->user), $this->getToken());
149455aa67eSAndreas Gohr    }
150455aa67eSAndreas Gohr
151455aa67eSAndreas Gohr    /**
152455aa67eSAndreas Gohr     * Get the user of this token
153455aa67eSAndreas Gohr     *
154455aa67eSAndreas Gohr     * @return string
155455aa67eSAndreas Gohr     */
156455aa67eSAndreas Gohr    public function getUser()
157455aa67eSAndreas Gohr    {
158455aa67eSAndreas Gohr        return $this->user;
159455aa67eSAndreas Gohr    }
160455aa67eSAndreas Gohr
161455aa67eSAndreas Gohr    /**
162455aa67eSAndreas Gohr     * Get the issued timestamp of this token
163455aa67eSAndreas Gohr     *
164455aa67eSAndreas Gohr     * @return int
165455aa67eSAndreas Gohr     */
166455aa67eSAndreas Gohr    public function getIssued()
167455aa67eSAndreas Gohr    {
168455aa67eSAndreas Gohr        return $this->issued;
169455aa67eSAndreas Gohr    }
170403d6a9fSAndreas Gohr
171403d6a9fSAndreas Gohr    /**
172403d6a9fSAndreas Gohr     * Get the storage file for this token
173403d6a9fSAndreas Gohr     *
174403d6a9fSAndreas Gohr     * Tokens are stored to be able to invalidate them
175403d6a9fSAndreas Gohr     *
176403d6a9fSAndreas Gohr     * @param string $user The user the token is for
177403d6a9fSAndreas Gohr     * @return string
178403d6a9fSAndreas Gohr     */
179403d6a9fSAndreas Gohr    public static function getStorageFile($user)
180403d6a9fSAndreas Gohr    {
181403d6a9fSAndreas Gohr        return getCacheName($user, '.token');
182403d6a9fSAndreas Gohr    }
183455aa67eSAndreas Gohr}
184