Completed
Push — 4-cactus ( 59c50f...21a27c )
by Alberto
02:40
created

plugins/BEdita/API/src/Auth/JwtAuthenticate.php (2 issues)

1
<?php
2
/**
3
 * BEdita, API-first content management framework
4
 * Copyright 2016 ChannelWeb Srl, Chialab Srl
5
 *
6
 * This file is part of BEdita: you can redistribute it and/or modify
7
 * it under the terms of the GNU Lesser General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
12
 */
13
14
namespace BEdita\API\Auth;
15
16
use BEdita\API\Exception\ExpiredTokenException;
17
use Cake\Auth\BaseAuthenticate;
18
use Cake\Http\Response;
19
use Cake\Http\ServerRequest;
20
use Cake\Network\Exception\UnauthorizedException;
21
use Cake\Routing\Router;
22
use Cake\Utility\Security;
23
use Firebase\JWT\JWT;
24
25
/**
26
 * An authentication adapter for authenticating using JSON Web Tokens.
27
 *
28
 * ```
29
 *  $this->Auth->config('authenticate', [
30
 *      'BEdita/Auth.Jwt' => [
31
 *          'parameter' => 'token',
32
 *          'userModel' => 'Users',
33
 *          'fields' => [
34
 *              'username' => 'id',
35
 *          ],
36
 *      ],
37
 *  ]);
38
 * ```
39
 *
40
 * @see http://jwt.io
41
 * @see http://tools.ietf.org/html/draft-ietf-oauth-json-web-token
42
 *
43
 * @since 4.0.0
44
 */
45
class JwtAuthenticate extends BaseAuthenticate
46
{
47
48
    /**
49
     * Default config for this object.
50
     *
51
     * - `header` The header where the token is stored. Defaults to `'Authorization'`.
52
     * - `headerPrefix` The prefix to the token in header. Defaults to `'Bearer'`.
53
     * - `queryParam` The query parameter where the token is passed as a fallback. Defaults to `'token'`.
54
     * - `allowedAlgorithms` List of supported verification algorithms. Defaults to `['HS256']`.
55
     *   See API of JWT::decode() for more info.
56
     * - `fields` The fields to use to identify a user by.
57
     * - `userModel` The alias for users table, defaults to Users.
58
     * - `finder` The finder method to use to fetch user record. Defaults to 'all'.
59
     *   You can set finder name as string or an array where key is finder name and value
60
     *   is an array passed to `Table::find()` options.
61
     *   E.g. ['finderName' => ['some_finder_option' => 'some_value']]
62
     * - `passwordHasher` Password hasher class. Can be a string specifying class name
63
     *    or an array containing `className` key, any other keys will be passed as
64
     *    config to the class. Defaults to 'Default'.
65
     * - Options `scope` and `contain` have been deprecated since 3.1. Use custom
66
     *   finder instead to modify the query to fetch user record.
67
     *
68
     * @var array
69
     */
70
    protected $_defaultConfig = [
71
        'header' => 'Authorization',
72
        'headerPrefix' => 'Bearer',
73
        'queryParam' => 'token',
74
        'allowedAlgorithms' => [
75
            'HS256',
76
            'HS512',
77
        ],
78
        'fields' => [
79
            'username' => 'id',
80
            'password' => null,
81
        ],
82
        'userModel' => 'Users',
83
        'scope' => [],
84
        'finder' => 'all',
85
        'contain' => null,
86
        'passwordHasher' => 'Default',
87
        'queryDatasource' => false,
88
    ];
89
90
    /**
91
     * Parsed token.
92
     *
93
     * @var string|null
94
     */
95
    protected $token = null;
96
97
    /**
98
     * Payload data.
99
     *
100
     * @var object|null
101
     */
102
    protected $payload = null;
103
104
    /**
105
     * Exception.
106
     *
107
     * @var \Exception
108
     */
109
    protected $error;
110
111
    /**
112
     * Get user record based on info available in JWT.
113
     *
114
     * @param \Cake\Http\ServerRequest $request The request object.
115
     * @param \Cake\Http\Response $response Response object.
116
     * @return array|false User record array or false on failure.
117
     */
118
    public function authenticate(ServerRequest $request, Response $response)
119
    {
120
        return $this->getUser($request);
121
    }
122
123
    /**
124
     * Get user record based on info available in JWT.
125
     *
126
     * @param \Cake\Http\ServerRequest $request Request object.
127
     * @return array|false User record array, `false` on failure.
128
     */
129
    public function getUser(ServerRequest $request)
130
    {
131
        $payload = $this->getPayload($request);
132
133
        if (!empty($this->error)) {
134
            throw new UnauthorizedException($this->error->getMessage());
135
        }
136
137
        if (!$this->_config['queryDatasource'] && !isset($payload['sub'])) {
138
            return $payload;
139
        }
140
141
        if (!isset($payload['sub'])) {
142
            return false;
143
        }
144
145
        $user = $this->_findUser($payload['sub']);
146
147
        return $user;
148
    }
149
150
    /**
151
     * Get payload data.
152
     *
153
     * @param \Cake\Http\ServerRequest $request Request instance or null
154
     * @return object|false Payload object on success, `false` on failure.
155
     * @throws \Exception Throws an exception if the token could not be decoded and debug is active.
156
     */
157
    public function getPayload(ServerRequest $request)
158
    {
159
        $token = $this->getToken($request);
160
        if ($token) {
161
            return $this->payload = $this->decode($token, $request);
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->decode($token, $request) of type false or array is incompatible with the declared type object|null of property $payload.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
162
        }
163
164
        return false;
165
    }
166
167
    /**
168
     * Get token from header or query string.
169
     *
170
     * @param \Cake\Http\ServerRequest $request Request object.
171
     * @return string|null Token string if found else null.
172
     */
173
    public function getToken(ServerRequest $request)
174
    {
175
        $config = $this->_config;
176
177
        $header = trim($request->getHeaderLine($config['header']));
178
        $headerPrefix = strtolower(trim($config['headerPrefix'])) . ' ';
179
        $headerPrefixLength = strlen($headerPrefix);
180
        if ($header && strtolower(substr($header, 0, $headerPrefixLength)) == $headerPrefix) {
181
            return $this->token = substr($header, $headerPrefixLength);
182
        }
183
184
        if (!empty($this->_config['queryParam'])) {
185
            return $this->token = $request->getQuery($this->_config['queryParam']);
0 ignored issues
show
Documentation Bug introduced by
It seems like $request->getQuery($this->_config['queryParam']) can also be of type array. However, the property $token is declared as type null|string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
186
        }
187
188
        return null;
189
    }
190
191
    /**
192
     * Decode JWT token.
193
     *
194
     * @param string $token JWT token to decode.
195
     * @param \Cake\Http\ServerRequest $request Request object.
196
     * @return array|false The token's payload as a PHP object, `false` on failure.
197
     * @throws \Exception Throws an exception if the token could not be decoded and debug is active.
198
     */
199
    protected function decode($token, ServerRequest $request)
200
    {
201
        try {
202
            $payload = JWT::decode($token, Security::getSalt(), $this->_config['allowedAlgorithms']);
203
204
            if (isset($payload->aud)) {
205
                $audience = Router::url($payload->aud, true);
206
                if (strpos($audience, Router::reverse($request, true)) !== 0) {
207
                    throw new \DomainException('Invalid audience');
208
                }
209
            }
210
211
            return (array)$payload;
212
        } catch (\Firebase\JWT\ExpiredException $e) {
213
            throw new ExpiredTokenException();
214
        } catch (\Exception $e) {
215
            $this->error = $e;
216
        }
217
218
        return false;
219
    }
220
221
    /**
222
     * Handles an unauthenticated access attempt.
223
     *
224
     * @param \Cake\Http\ServerRequest $request A request object.
225
     * @param \Cake\Http\Response $response A response object.
226
     * @return void
227
     * @throws \Cake\Network\Exception\UnauthorizedException Throws an exception.
228
     */
229
    public function unauthenticated(ServerRequest $request, Response $response)
230
    {
231
        $message = $this->_registry->getController()->Auth->getConfig('authError');
232
        if ($this->error) {
233
            $message = $this->error->getMessage();
234
        }
235
236
        throw new UnauthorizedException($message);
237
    }
238
}
239