ApiAuth   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 146
Duplicated Lines 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
wmc 27
eloc 58
c 2
b 0
f 1
dl 0
loc 146
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A before() 0 11 2
A createJWT() 0 13 2
A getClaimedScopes() 0 13 3
A authenticate() 0 7 2
A shouldGenerate() 0 3 2
A serve() 0 14 2
A validateGenerate() 0 11 4
A validateScope() 0 17 4
A generateToken() 0 13 4
A __construct() 0 11 2
1
<?php
2
3
/*
4
 * This file is part of the PHALCON-EXT package.
5
 *
6
 * (c) Jitendra Adhikari <[email protected]>
7
 *     <https://github.com/adhocore>
8
 *
9
 * Licensed under MIT license.
10
 */
11
12
namespace PhalconExt\Http\Middleware;
13
14
use Phalcon\Http\Request;
15
use Phalcon\Http\Response;
16
use PhalconExt\Contract\ApiAuthenticator;
17
use PhalconExt\Factory\ApiAuthenticator as FactoryAuthenticator;
18
use PhalconExt\Http\BaseMiddleware;
19
20
/**
21
 * Check authentication &/or authorization for api requests.
22
 *
23
 * @author  Jitendra Adhikari <[email protected]>
24
 * @license MIT
25
 *
26
 * @link    https://github.com/adhocore/phalcon-ext
27
 */
28
class ApiAuth extends BaseMiddleware
29
{
30
    protected $configKey = 'apiAuth';
31
32
    /** @var ApiAuthenticator */
33
    protected $authenticator;
34
35
    public function __construct(ApiAuthenticator $authenticator = null)
36
    {
37
        parent::__construct();
38
39
        $this->authenticator = $authenticator ?? $this->di(FactoryAuthenticator::class);
40
41
        if (!$this->di()->has('authenticator')) {
42
            $this->di()->setShared('authenticator', $this->authenticator);
43
        }
44
45
        $this->authenticator->configure($this->config);
46
    }
47
48
    /**
49
     * Handle authentication.
50
     *
51
     * @param Request  $request
52
     * @param Response $response
53
     *
54
     * @return bool
55
     */
56
    public function before(Request $request, Response $response): bool
0 ignored issues
show
Unused Code introduced by
The parameter $response is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

56
    public function before(Request $request, /** @scrutinizer ignore-unused */ Response $response): bool

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
57
    {
58
        list($routeName, $currentUri) = $this->getRouteNameUri();
59
60
        if ($this->shouldGenerate($request, $currentUri)) {
61
            return $this->generateToken($request);
62
        }
63
64
        $requiredScope = $this->config['scopes'][$routeName] ?? $this->config['scopes'][$currentUri] ?? null;
65
66
        return $this->validateScope($request, $requiredScope);
67
    }
68
69
    protected function shouldGenerate(Request $request, string $currentUri): bool
70
    {
71
        return $request->isPost() && $this->config['authUri'] === $currentUri;
72
    }
73
74
    protected function generateToken(Request $request)
75
    {
76
        $payload = $request->getJsonRawBody(true) ?: $request->getPost();
77
78
        if (!$this->validateGenerate($payload)) {
79
            return $this->abort(401, 'Credentials missing');
80
        }
81
82
        if (!$this->authenticate($payload)) {
83
            return $this->abort(401, 'Credentials invalid');
84
        }
85
86
        return $this->serve($payload['grant_type']);
87
    }
88
89
    protected function validateGenerate(array $payload): bool
90
    {
91
        if (!isset($payload['grant_type'], $payload[$payload['grant_type']])) {
92
            return false;
93
        }
94
95
        if (!\in_array($payload['grant_type'], ['password', 'refresh_token'])) {
96
            return false;
97
        }
98
99
        return 'password' !== $payload['grant_type'] || isset($payload['username']);
100
    }
101
102
    protected function authenticate(array $payload): bool
103
    {
104
        if ('refresh_token' === $payload['grant_type']) {
105
            return $this->authenticator->byRefreshToken($payload['refresh_token']);
106
        }
107
108
        return $this->authenticator->byCredential($payload['username'], $payload['password']);
109
    }
110
111
    protected function serve(string $grantType): bool
112
    {
113
        $token = [
114
            'access_token' => $this->createJWT(),
115
            'token_type'   => 'bearer',
116
            'expires_in'   => $this->config['jwt']['maxAge'],
117
            'grant_type'   => $grantType,
118
        ];
119
120
        if ($grantType === 'password') {
121
            $token['refresh_token'] = $this->authenticator->createRefreshToken();
122
        }
123
124
        return $this->abort(200, $token);
125
    }
126
127
    protected function createJWT(): string
128
    {
129
        $jwt = [
130
            'sub'    => $this->authenticator->getSubject(),
131
            'scopes' => $this->authenticator->getScopes(),
132
        ];
133
134
        if ($this->config['jwt']['issuer'] ?? null) {
135
            $jwt['iss'] = $this->config['jwt']['issuer'];
136
        }
137
138
        // 'exp' is automatically added based on `config->apiAuth->jwt->maxAge`!
139
        return $this->di('jwt')->encode($jwt);
140
    }
141
142
    protected function validateScope(Request $request, string $requiredScope = null): bool
143
    {
144
        $claimedScopes = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $claimedScopes is dead and can be removed.
Loading history...
145
        $msg           = 'Permission denied';
146
        $jwt           = $request->getHeader('Authorization');
147
148
        try {
149
            $claimedScopes = $this->getClaimedScopes($jwt);
150
        } catch (\InvalidArgumentException $e) {
151
            $msg = $e->getMessage();
152
        }
153
154
        if ($requiredScope && !\in_array($requiredScope, $claimedScopes)) {
155
            return $this->abort(403, $msg);
156
        }
157
158
        return true;
159
    }
160
161
    protected function getClaimedScopes(string $jwt = null): array
162
    {
163
        if ('' === $jwt = \str_replace('Bearer ', '', \trim($jwt))) {
0 ignored issues
show
Bug introduced by
It seems like $jwt can also be of type null; however, parameter $string of trim() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

163
        if ('' === $jwt = \str_replace('Bearer ', '', \trim(/** @scrutinizer ignore-type */ $jwt))) {
Loading history...
164
            return [];
165
        }
166
167
        $decoded = $this->di('jwt')->decode($jwt);
168
169
        if ($decoded['sub'] ?? null) {
170
            $this->authenticator->bySubject($decoded['sub']);
171
        }
172
173
        return $decoded['scopes'] ?? [];
174
    }
175
}
176