Passed
Push — master ( 80605f...a8b319 )
by Hergen
05:20
created

JwtAuthRoles::authUser()   A

Complexity

Conditions 5
Paths 12

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 26
dl 0
loc 35
c 0
b 0
f 0
rs 9.1928
cc 5
nc 12
nop 1
1
<?php
2
3
namespace Werk365\JwtAuthRoles;
4
5
use Firebase\JWT\JWT;
6
use Illuminate\Support\Facades\Http;
0 ignored issues
show
Bug introduced by
The type Illuminate\Support\Facades\Http was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
7
use Illuminate\Support\Str;
8
use phpseclib\Crypt\RSA;
9
use phpseclib\Math\BigInteger;
10
use Werk365\JwtAuthRoles\Exceptions\AuthException;
11
use Werk365\JwtAuthRoles\Models\JwtKey;
12
use Werk365\JwtAuthRoles\Models\JwtUser;
13
14
class JwtAuthRoles
15
{
16
    private static function getKid(string $jwt): ?string
17
    {
18
        if (!Str::is('*.*.*', $jwt)) {
19
            throw AuthException::auth(422, 'Malformed JWT');
20
        }
21
22
        $header = JWT::jsonDecode(JWT::urlsafeB64Decode(Str::before($jwt, '.')));
23
24
        if (isset($header->alg) && $header->alg !== config('jwtauthroles.alg')) {
25
            throw AuthException::auth(422, 'Invalid algorithm');
26
        }
27
28
        return $header->kid ?? null;
29
    }
30
31
    private static function getClaims(string $jwt): ?object
32
    {
33
        if (!Str::is('*.*.*', $jwt)) {
34
            throw AuthException::auth(422, 'Malformed JWT');
35
        }
36
37
        $claims = explode('.', $jwt);
38
        $claims = JWT::jsonDecode(JWT::urlsafeB64Decode($claims[1]));
39
40
        return $claims ?? null;
41
    }
42
43
    private static function jwkToPem(object $jwk): ?string
44
    {
45
        if (!isset($jwk->e) || !isset($jwk->n)) {
46
            throw AuthException::auth(500, 'Malformed jwk');
47
        }
48
49
        $rsa = new RSA();
50
        $rsa->loadKey([
51
            'e' => new BigInteger(JWT::urlsafeB64Decode($jwk->e), 256),
52
            'n' => new BigInteger(JWT::urlsafeB64Decode($jwk->n), 256),
53
        ]);
54
55
        return $rsa->getPublicKey();
0 ignored issues
show
Bug Best Practice introduced by
The expression return $rsa->getPublicKey() could return the type false which is incompatible with the type-hinted return null|string. Consider adding an additional type-check to rule them out.
Loading history...
56
    }
57
58
    private static function getJwk(string $kid, string $uri): ?string
59
    {
60
        $response = Http::get($uri);
61
        $json = $response->getBody();
62
        if (!$json) {
63
            throw AuthException::auth(404, 'jwks endpoint not found');
64
        }
65
66
        $jwks = json_decode($json, false);
67
68
        if (!$jwks || !isset($jwks->keys) || !is_array($jwks->keys)) {
69
            throw AuthException::auth(404, 'No JWKs found');
70
        }
71
72
        foreach ($jwks->keys as $jwk) {
73
            if ($jwk->kid === $kid) {
74
                return self::jwkToPem($jwk);
75
            }
76
        }
77
78
        throw AuthException::auth(401, 'Unauthorized');
79
    }
80
81
    private static function getPem(string $kid, string $uri): ?string
82
    {
83
        $response = Http::get($uri);
84
        $json = $response->getBody();
85
        if (!$json) {
86
            throw AuthException::auth(404, 'pem endpoint not found');
87
        }
88
89
        $pems = json_decode($json, false);
90
91
        if (!$pems || !isset($pems->publicKeys) || !is_object($pems->publicKeys)) {
92
            throw AuthException::auth(404, 'pem not found');
93
        }
94
95
        foreach ($pems->publicKeys as $key=>$pem) {
96
            if ($key === $kid) {
97
                return $pem;
98
            }
99
        }
100
101
        throw AuthException::auth(401, 'Unauthorized');
102
    }
103
104
    private static function verifyToken(string $jwt, string $uri, bool $jwk = false): object
105
    {
106
        $kid = self::getKid($jwt);
107
        if (! $kid) {
108
            throw AuthException::auth(422, 'Malformed JWT');
109
        }
110
        if (config('jwtauthroles.cache.enabled')) {
111
            if (config('jwtauthroles.cache.type') === 'database') {
112
                $row = JwtKey::where('kid', $kid)
113
                    ->orderBy('created_at', 'desc')
114
                    ->first('key');
115
            }
116
        }
117
118
        $publicKey = $row->key
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $row does not seem to be defined for all execution paths leading up to this point.
Loading history...
119
            ?? $jwk
120
                ? self::getJwk($kid, $uri)
121
                : self::getPem($kid, $uri);
122
123
        if (! isset($publicKey) || ! $publicKey) {
124
            throw AuthException::auth(500, 'Unable to validate JWT');
125
        }
126
127
        if (config('jwtauthroles.cache.enabled')) {
128
            if (config('jwtauthroles.cache.type') === 'database') {
129
                $row = $row ?? JwtKey::create(['kid' => $kid, 'key' => $publicKey]);
0 ignored issues
show
Unused Code introduced by
The assignment to $row is dead and can be removed.
Loading history...
130
            }
131
        }
132
133
        return JWT::decode($jwt, $publicKey, [config('jwtauthroles.alg')]);
134
    }
135
136
    public static function authUser(object $request)
137
    {
138
        $jwt = $request->bearerToken();
139
140
        $uri = config('jwtauthroles.useJwk')
141
            ? config('jwtauthroles.jwkUri')
142
            : config('jwtauthroles.pemUri');
143
144
        if (! config('jwtauthroles.validateJwt')) {
145
            $claims = self::getClaims($jwt);
146
        } else {
147
            $claims = self::verifyToken($jwt, $uri, config('jwtauthroles.useJwk'));
148
        }
149
150
        if (config('jwtauthroles.useDB')) {
151
            if (config('jwtauthroles.autoCreateUser')) {
152
                $user = JwtUser::firstOrNew([config('jwtauthroles.userId') => $claims->sub]);
153
                $user[config('jwtauthroles.userId')] = $claims->sub;
154
                $user->roles = json_encode($claims->roles);
155
                $user->claims = json_encode($claims);
156
                $user->save();
157
            } else {
158
                $user = JwtUser::where(config('jwtauthroles.userId'), '=', $claims->sub)->firstOrFail();
159
                $user->roles = json_encode($claims->roles);
160
                $user->claims = json_encode($claims);
161
                $user->save();
162
            }
163
        } else {
164
            $user = new JwtUser;
165
            $user->uuid = $claims->sub;
166
            $user->roles = $claims->roles;
167
            $user->claims = $claims;
168
        }
169
170
        return $user;
171
    }
172
}
173