Passed
Pull Request — master (#26)
by Jitendra
01:50
created

ApiAuth::validateScope()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 17
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 17
rs 9.2
c 0
b 0
f 0
cc 4
eloc 10
nc 4
nop 2
1
<?php
2
3
namespace PhalconExt\Http\Middleware;
4
5
use Phalcon\Http\Request;
6
use Phalcon\Http\Response;
7
use PhalconExt\Contract\ApiAuthenticator;
8
use PhalconExt\Factory\ApiAuthenticator as FactoryAuthenticator;
9
use PhalconExt\Http\BaseMiddleware;
10
11
/**
12
 * Check authentication &/or authorization for api requests.
13
 *
14
 * @author  Jitendra Adhikari <[email protected]>
15
 * @license MIT
16
 *
17
 * @link    https://github.com/adhocore/phalcon-ext
18
 */
19
class ApiAuth extends BaseMiddleware
20
{
21
    protected $configKey = 'apiAuth';
22
23
    /** @var ApiAuthenticator */
24
    protected $authenticator;
25
26
    public function __construct(ApiAuthenticator $authenticator = null)
27
    {
28
        parent::__construct();
29
30
        $this->authenticator = $authenticator ?? $this->di(FactoryAuthenticator::class);
31
32
        if (!$this->di()->has('authenticator')) {
33
            $this->di()->setShared('authenticator', $this->authenticator);
34
        }
35
36
        $this->authenticator->configure($this->config);
37
    }
38
39
    /**
40
     * Handle authentication.
41
     *
42
     * @param Request  $request
43
     * @param Response $response
44
     *
45
     * @return bool
46
     */
47
    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

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