Passed
Pull Request — master (#26)
by Jitendra
02:07
created

ApiAuth::getClaimedScopes()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 13
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 3
nop 1
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\Http\BaseMiddleware;
9
use PhalconExt\Factory\ApiAuthenticator as FactoryAuthenticator;
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
    protected $authenticator;
24
25
    public function __construct(ApiAuthenticator $authenticator = null)
26
    {
27
        parent::__construct();
28
29
        $this->authenticator = $authenticator ?? $this->di(FactoryAuthenticator::class);
30
31
        if (!$this->di()->has('authenticator')) {
32
            $this->di()->setShared('authenticator', $this->authenticator);
33
        }
34
35
        $this->authenticator->configure($this->config);
36
    }
37
38
    /**
39
     * Handle authentication.
40
     *
41
     * @param Request  $request
42
     * @param Response $response
43
     *
44
     * @return bool
45
     */
46
    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

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