Completed
Push — master ( 592d95...c00565 )
by Abdelrahman
18:11 queued 10s
created

TokenGuard::authenticateViaBearerToken()   B

Complexity

Conditions 9
Paths 10

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 7.7564
c 0
b 0
f 0
cc 9
nc 10
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Oauth\Guards;
6
7
use Exception;
8
use Firebase\JWT\JWT;
9
use Illuminate\Http\Request;
10
use Rinvex\Oauth\TransientToken;
11
use Rinvex\Oauth\OAuthUserProvider;
12
use Nyholm\Psr7\Factory\Psr17Factory;
13
use Illuminate\Cookie\CookieValuePrefix;
14
use League\OAuth2\Server\ResourceServer;
15
use Illuminate\Contracts\Encryption\Encrypter;
16
use Illuminate\Contracts\Debug\ExceptionHandler;
17
use Illuminate\Cookie\Middleware\EncryptCookies;
18
use League\OAuth2\Server\Exception\OAuthServerException;
19
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
20
21
class TokenGuard
22
{
23
    /**
24
     * The resource server instance.
25
     *
26
     * @var \League\OAuth2\Server\ResourceServer
27
     */
28
    protected $server;
29
30
    /**
31
     * The user provider implementation.
32
     *
33
     * @var \Rinvex\Oauth\OAuthUserProvider
34
     */
35
    protected $provider;
36
37
    /**
38
     * The encrypter implementation.
39
     *
40
     * @var \Illuminate\Contracts\Encryption\Encrypter
41
     */
42
    protected $encrypter;
43
44
    /**
45
     * Create a new token guard instance.
46
     *
47
     * @param \League\OAuth2\Server\ResourceServer       $server
48
     * @param \Rinvex\Oauth\OAuthUserProvider            $provider
49
     * @param \Illuminate\Contracts\Encryption\Encrypter $encrypter
50
     *
51
     * @return void
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
52
     */
53
    public function __construct(ResourceServer $server, OAuthUserProvider $provider, Encrypter $encrypter)
54
    {
55
        $this->server = $server;
56
        $this->provider = $provider;
57
        $this->encrypter = $encrypter;
58
    }
59
60
    /**
61
     * Determine if the requested user type matches the client's user type.
62
     *
63
     * @param \Illuminate\Http\Request $request
64
     *
65
     * @return bool
66
     */
67
    protected function hasValidUserType(Request $request)
68
    {
69
        $client = $this->client($request);
70
71
        if ($client && ! $client->user_type) {
72
            return true;
73
        }
74
75
        return $client && $client->user_type === $this->provider->getUserType();
76
    }
77
78
    /**
79
     * Get the user for the incoming request.
80
     *
81
     * @param \Illuminate\Http\Request $request
82
     *
83
     * @return mixed
84
     */
85
    public function user(Request $request)
86
    {
87
        if ($request->bearerToken()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->bearerToken() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
88
            return $this->authenticateViaBearerToken($request);
89
        } elseif ($request->cookie(config('rinvex.oauth.cookie'))) {
90
            return $this->authenticateViaCookie($request);
91
        }
92
    }
93
94
    /**
95
     * Get the client for the incoming request.
96
     *
97
     * @param \Illuminate\Http\Request $request
98
     *
99
     * @return mixed
100
     */
101
    public function client(Request $request)
102
    {
103
        if ($request->bearerToken()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $request->bearerToken() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
104
            if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
105
                return;
106
            }
107
108
            $client = app('rinvex.oauth.client')->resolveRouteBinding($psr->getAttribute('oauth_client_id'));
109
110
            return $client && ! $client->is_revoked ? $client : null;
111
        } elseif ($request->cookie(config('rinvex.oauth.cookie'))) {
112
            if ($token = $this->getTokenViaCookie($request)) {
113
                $client = app('rinvex.oauth.client')->resolveRouteBinding($token['aud']);
114
115
                return $client && ! $client->is_revoked ? $client : null;
116
            }
117
        }
118
    }
119
120
    /**
121
     * Authenticate the incoming request via the Bearer token.
122
     *
123
     * @param \Illuminate\Http\Request $request
124
     *
125
     * @return mixed
126
     */
127
    protected function authenticateViaBearerToken($request)
128
    {
129
        if (! $psr = $this->getPsrRequestViaBearerToken($request)) {
130
            return;
131
        }
132
133
        if (! $this->hasValidUserType($request)) {
134
            return;
135
        }
136
137
        // If the access token is valid we will retrieve the user according to the user ID
138
        // associated with the token. We will use the provider implementation which may
139
        // be used to retrieve users from Eloquent. Next, we'll be ready to continue.
140
        [$userType, $userId] = explode(':', $psr->getAttribute('oauth_user_id'));
0 ignored issues
show
Bug introduced by
The variable $userType does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
Bug introduced by
The variable $userId seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
141
        $userId = method_exists($user = app('cortex.auth.'.$userType), 'unhashId') ? $user->unhashId($userId) : $userId;
0 ignored issues
show
Bug introduced by
The variable $userId seems only to be defined at a later point. Did you maybe move this code here without moving the variable definition?

This error can happen if you refactor code and forget to move the variable initialization.

Let’s take a look at a simple example:

function someFunction() {
    $x = 5;
    echo $x;
}

The above code is perfectly fine. Now imagine that we re-order the statements:

function someFunction() {
    echo $x;
    $x = 5;
}

In that case, $x would be read before it is initialized. This was a very basic example, however the principle is the same for the found issue.

Loading history...
142
        $user = $this->provider->retrieveById($userId ?: null);
143
144
        if (! $user) {
145
            return;
146
        }
147
148
        // Next, we will assign a token instance to this user which the developers may use
149
        // to determine if the token has a given scope, etc. This will be useful during
150
        // authorization such as within the developer's Laravel model policy classes.
151
        $token = app('rinvex.oauth.access_token')->where('identifier', $psr->getAttribute('oauth_access_token_id'))->first();
152
        $clientId = $psr->getAttribute('oauth_client_id');
153
154
        // Finally, we will verify if the client that issued this token is still valid and
155
        // its tokens may still be used. If not, we will bail out since we don't want a
156
        // user to be able to send access tokens for deleted or revoked applications.
157
        $client = app('rinvex.oauth.client')->resolveRouteBinding($clientId);
158
159
        if (is_null($client) || $client->is_revoked) {
160
            return;
161
        }
162
163
        return $token ? $user->withAccessToken($token) : null;
0 ignored issues
show
Bug introduced by
The method withAccessToken() does not seem to exist on object<Illuminate\Contracts\Auth\Authenticatable>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
164
    }
165
166
    /**
167
     * Authenticate and get the incoming PSR-7 request via the Bearer token.
168
     *
169
     * @param \Illuminate\Http\Request $request
170
     *
171
     * @return \Psr\Http\Message\ServerRequestInterface
0 ignored issues
show
Documentation introduced by
Should the return type not be \Psr\Http\Message\ServerRequestInterface|null?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
172
     */
173
    protected function getPsrRequestViaBearerToken($request)
174
    {
175
        // First, we will convert the Symfony request to a PSR-7 implementation which will
176
        // be compatible with the base OAuth2 library. The Symfony bridge can perform a
177
        // conversion for us to a new Nyholm implementation of this PSR-7 request.
178
        $psr = (new PsrHttpFactory(
179
            new Psr17Factory(),
180
            new Psr17Factory(),
181
            new Psr17Factory(),
182
            new Psr17Factory()
183
        ))->createRequest($request);
184
185
        try {
186
            return $this->server->validateAuthenticatedRequest($psr);
187
        } catch (OAuthServerException $e) {
188
            $request->headers->set('Authorization', '', true);
189
190
            app(ExceptionHandler::class)->report($e);
191
        }
192
    }
193
194
    /**
195
     * Authenticate the incoming request via the token cookie.
196
     *
197
     * @param \Illuminate\Http\Request $request
198
     *
199
     * @return mixed
200
     */
201
    protected function authenticateViaCookie($request)
202
    {
203
        if (! $token = $this->getTokenViaCookie($request)) {
204
            return;
205
        }
206
207
        // If this user exists, we will return this user and attach a "transient" token to
208
        // the user model. The transient token assumes it has all scopes since the user
209
        // is physically logged into the application via the application's interface.
210
        if ($user = $this->provider->retrieveById($token['sub'])) {
211
            return $user->withAccessToken(new TransientToken());
0 ignored issues
show
Bug introduced by
The method withAccessToken() does not seem to exist on object<Illuminate\Contracts\Auth\Authenticatable>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
212
        }
213
    }
214
215
    /**
216
     * Get the token cookie via the incoming request.
217
     *
218
     * @param \Illuminate\Http\Request $request
219
     *
220
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use null|array.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
221
     */
222
    protected function getTokenViaCookie($request)
223
    {
224
        // If we need to retrieve the token from the cookie, it'll be encrypted so we must
225
        // first decrypt the cookie and then attempt to find the token value within the
226
        // database. If we can't decrypt the value we'll bail out with a null return.
227
        try {
228
            $token = $this->decodeJwtTokenCookie($request);
229
        } catch (Exception $e) {
230
            return;
231
        }
232
233
        // We will compare the CSRF token in the decoded API token against the CSRF header
234
        // sent with the request. If they don't match then this request isn't sent from
235
        // a valid source and we won't authenticate the request for further handling.
236
        if (! config('rinvex.oauth.ignore_csrf_token') && (! $this->validCsrf($token, $request) ||
237
            time() >= $token['expiry'])) {
238
            return;
239
        }
240
241
        return $token;
242
    }
243
244
    /**
245
     * Decode and decrypt the JWT token cookie.
246
     *
247
     * @param \Illuminate\Http\Request $request
248
     *
249
     * @return array
250
     */
251
    protected function decodeJwtTokenCookie($request)
252
    {
253
        return (array) JWT::decode(
254
            CookieValuePrefix::remove($this->encrypter->decrypt($request->cookie(config('rinvex.oauth.cookie')), config('rinvex.oauth.unserializes_cookies'))),
255
            $this->encrypter->getKey(),
256
            ['HS256']
257
        );
258
    }
259
260
    /**
261
     * Determine if the CSRF / header are valid and match.
262
     *
263
     * @param array                    $token
264
     * @param \Illuminate\Http\Request $request
265
     *
266
     * @return bool
267
     */
268
    protected function validCsrf($token, $request)
269
    {
270
        return isset($token['csrf']) && hash_equals(
271
            $token['csrf'],
272
            (string) $this->getTokenFromRequest($request)
273
        );
274
    }
275
276
    /**
277
     * Get the CSRF token from the request.
278
     *
279
     * @param \Illuminate\Http\Request $request
280
     *
281
     * @return string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|array|null? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
282
     */
283
    protected function getTokenFromRequest($request)
284
    {
285
        $token = $request->header('X-CSRF-TOKEN');
286
287
        if (! $token && $header = $request->header('X-XSRF-TOKEN')) {
288
            $token = CookieValuePrefix::remove($this->encrypter->decrypt($header, static::serialized()));
0 ignored issues
show
Bug introduced by
It seems like $header defined by $request->header('X-XSRF-TOKEN') on line 287 can also be of type array; however, Illuminate\Contracts\Enc...on\Encrypter::decrypt() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
289
        }
290
291
        return $token;
292
    }
293
294
    /**
295
     * Determine if the cookie contents should be serialized.
296
     *
297
     * @return bool
298
     */
299
    public static function serialized()
300
    {
301
        return EncryptCookies::serialized('XSRF-TOKEN');
302
    }
303
}
304