xref: /dokuwiki/inc/JWT.php (revision 455aa67e850e236be8cd442e32eec2b8fff15fb2)
1*455aa67eSAndreas Gohr<?php
2*455aa67eSAndreas Gohr
3*455aa67eSAndreas Gohrnamespace dokuwiki;
4*455aa67eSAndreas Gohr
5*455aa67eSAndreas Gohr/**
6*455aa67eSAndreas Gohr * Minimal JWT implementation
7*455aa67eSAndreas Gohr */
8*455aa67eSAndreas Gohrclass JWT
9*455aa67eSAndreas Gohr{
10*455aa67eSAndreas Gohr
11*455aa67eSAndreas Gohr    protected $user;
12*455aa67eSAndreas Gohr    protected $issued;
13*455aa67eSAndreas Gohr    protected $secret;
14*455aa67eSAndreas Gohr
15*455aa67eSAndreas Gohr    /**
16*455aa67eSAndreas Gohr     * Create a new JWT object
17*455aa67eSAndreas Gohr     *
18*455aa67eSAndreas Gohr     * Use validate() or create() to create a new instance
19*455aa67eSAndreas Gohr     *
20*455aa67eSAndreas Gohr     * @param string $user
21*455aa67eSAndreas Gohr     * @param int $issued
22*455aa67eSAndreas Gohr     */
23*455aa67eSAndreas Gohr    protected function __construct($user, $issued)
24*455aa67eSAndreas Gohr    {
25*455aa67eSAndreas Gohr        $this->user = $user;
26*455aa67eSAndreas Gohr        $this->issued = $issued;
27*455aa67eSAndreas Gohr    }
28*455aa67eSAndreas Gohr
29*455aa67eSAndreas Gohr    /**
30*455aa67eSAndreas Gohr     * Load the cookiesalt as secret
31*455aa67eSAndreas Gohr     *
32*455aa67eSAndreas Gohr     * @return string
33*455aa67eSAndreas Gohr     */
34*455aa67eSAndreas Gohr    protected static function getSecret()
35*455aa67eSAndreas Gohr    {
36*455aa67eSAndreas Gohr        return auth_cookiesalt(false, true);
37*455aa67eSAndreas Gohr    }
38*455aa67eSAndreas Gohr
39*455aa67eSAndreas Gohr    /**
40*455aa67eSAndreas Gohr     * Create a new instance from a token
41*455aa67eSAndreas Gohr     *
42*455aa67eSAndreas Gohr     * @param $token
43*455aa67eSAndreas Gohr     * @return self
44*455aa67eSAndreas Gohr     * @throws \Exception
45*455aa67eSAndreas Gohr     */
46*455aa67eSAndreas Gohr    public static function validate($token)
47*455aa67eSAndreas Gohr    {
48*455aa67eSAndreas Gohr        [$header, $payload, $signature] = sexplode('.', $token, 3);
49*455aa67eSAndreas Gohr        $signature = base64_decode($signature);
50*455aa67eSAndreas Gohr
51*455aa67eSAndreas Gohr        if (!hash_equals($signature, hash_hmac('sha256', "$header.$payload", self::getSecret(), true))) {
52*455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT signature');
53*455aa67eSAndreas Gohr        }
54*455aa67eSAndreas Gohr
55*455aa67eSAndreas Gohr        $header = json_decode(base64_decode($header), true);
56*455aa67eSAndreas Gohr        $payload = json_decode(base64_decode($payload), true);
57*455aa67eSAndreas Gohr
58*455aa67eSAndreas Gohr        if (!$header || !$payload || !$signature) {
59*455aa67eSAndreas Gohr            throw new \Exception('Invalid JWT');
60*455aa67eSAndreas Gohr        }
61*455aa67eSAndreas Gohr
62*455aa67eSAndreas Gohr        if ($header['alg'] !== 'HS256') {
63*455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT algorithm');
64*455aa67eSAndreas Gohr        }
65*455aa67eSAndreas Gohr        if ($header['typ'] !== 'JWT') {
66*455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT type');
67*455aa67eSAndreas Gohr        }
68*455aa67eSAndreas Gohr        if ($payload['iss'] !== 'dokuwiki') {
69*455aa67eSAndreas Gohr            throw new \Exception('Unsupported JWT issuer');
70*455aa67eSAndreas Gohr        }
71*455aa67eSAndreas Gohr        if (isset($payload['exp']) && $payload['exp'] < time()) {
72*455aa67eSAndreas Gohr            throw new \Exception('JWT expired');
73*455aa67eSAndreas Gohr        }
74*455aa67eSAndreas Gohr
75*455aa67eSAndreas Gohr        $user = $payload['sub'];
76*455aa67eSAndreas Gohr        $file = getCacheName($user, '.token');
77*455aa67eSAndreas Gohr        if (!file_exists($file)) {
78*455aa67eSAndreas Gohr            throw new \Exception('JWT not found, maybe it expired?');
79*455aa67eSAndreas Gohr        }
80*455aa67eSAndreas Gohr
81*455aa67eSAndreas Gohr        return new self($user, $payload['iat']);
82*455aa67eSAndreas Gohr    }
83*455aa67eSAndreas Gohr
84*455aa67eSAndreas Gohr    /**
85*455aa67eSAndreas Gohr     * Create a new instance from a user
86*455aa67eSAndreas Gohr     *
87*455aa67eSAndreas Gohr     * Loads an existing token if available
88*455aa67eSAndreas Gohr     *
89*455aa67eSAndreas Gohr     * @param $user
90*455aa67eSAndreas Gohr     * @return self
91*455aa67eSAndreas Gohr     */
92*455aa67eSAndreas Gohr    public static function fromUser($user)
93*455aa67eSAndreas Gohr    {
94*455aa67eSAndreas Gohr        $file = getCacheName($user, '.token');
95*455aa67eSAndreas Gohr
96*455aa67eSAndreas Gohr        if (file_exists($file)) {
97*455aa67eSAndreas Gohr            try {
98*455aa67eSAndreas Gohr                return self::validate(io_readFile($file));
99*455aa67eSAndreas Gohr            } catch (\Exception $ignored) {
100*455aa67eSAndreas Gohr            }
101*455aa67eSAndreas Gohr        }
102*455aa67eSAndreas Gohr
103*455aa67eSAndreas Gohr        $token = new self($user, time());
104*455aa67eSAndreas Gohr        $token->save();
105*455aa67eSAndreas Gohr        return $token;
106*455aa67eSAndreas Gohr    }
107*455aa67eSAndreas Gohr
108*455aa67eSAndreas Gohr
109*455aa67eSAndreas Gohr    /**
110*455aa67eSAndreas Gohr     * Get the JWT token for this instance
111*455aa67eSAndreas Gohr     *
112*455aa67eSAndreas Gohr     * @return string
113*455aa67eSAndreas Gohr     */
114*455aa67eSAndreas Gohr    public function getToken()
115*455aa67eSAndreas Gohr    {
116*455aa67eSAndreas Gohr        $header = [
117*455aa67eSAndreas Gohr            'alg' => 'HS256',
118*455aa67eSAndreas Gohr            'typ' => 'JWT',
119*455aa67eSAndreas Gohr        ];
120*455aa67eSAndreas Gohr        $header = base64_encode(json_encode($header));
121*455aa67eSAndreas Gohr        $payload = [
122*455aa67eSAndreas Gohr            'iss' => 'dokuwiki',
123*455aa67eSAndreas Gohr            'sub' => $this->user,
124*455aa67eSAndreas Gohr            'iat' => $this->issued,
125*455aa67eSAndreas Gohr        ];
126*455aa67eSAndreas Gohr        $payload = base64_encode(json_encode($payload));
127*455aa67eSAndreas Gohr        $signature = hash_hmac('sha256', "$header.$payload", self::getSecret(), true);
128*455aa67eSAndreas Gohr        $signature = base64_encode($signature);
129*455aa67eSAndreas Gohr        return "$header.$payload.$signature";
130*455aa67eSAndreas Gohr    }
131*455aa67eSAndreas Gohr
132*455aa67eSAndreas Gohr    /**
133*455aa67eSAndreas Gohr     * Save the token for the user
134*455aa67eSAndreas Gohr     *
135*455aa67eSAndreas Gohr     * Resets the issued timestamp
136*455aa67eSAndreas Gohr     */
137*455aa67eSAndreas Gohr    public function save()
138*455aa67eSAndreas Gohr    {
139*455aa67eSAndreas Gohr        $this->issued = time();
140*455aa67eSAndreas Gohr        $file = getCacheName($this->user, '.token');
141*455aa67eSAndreas Gohr        io_saveFile($file, $this->getToken());
142*455aa67eSAndreas Gohr    }
143*455aa67eSAndreas Gohr
144*455aa67eSAndreas Gohr    /**
145*455aa67eSAndreas Gohr     * Get the user of this token
146*455aa67eSAndreas Gohr     *
147*455aa67eSAndreas Gohr     * @return string
148*455aa67eSAndreas Gohr     */
149*455aa67eSAndreas Gohr    public function getUser()
150*455aa67eSAndreas Gohr    {
151*455aa67eSAndreas Gohr        return $this->user;
152*455aa67eSAndreas Gohr    }
153*455aa67eSAndreas Gohr
154*455aa67eSAndreas Gohr    /**
155*455aa67eSAndreas Gohr     * Get the issued timestamp of this token
156*455aa67eSAndreas Gohr     *
157*455aa67eSAndreas Gohr     * @return int
158*455aa67eSAndreas Gohr     */
159*455aa67eSAndreas Gohr    public function getIssued()
160*455aa67eSAndreas Gohr    {
161*455aa67eSAndreas Gohr        return $this->issued;
162*455aa67eSAndreas Gohr    }
163*455aa67eSAndreas Gohr}
164