Passed
Push — master ( 3e5b57...979fc2 )
by Hergen
05:00
created

JwtAuthRoles::jwkToPem()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 7
c 1
b 0
f 0
nc 2
nop 1
dl 0
loc 12
rs 10
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...
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
            $header = JWT::jsonDecode(JWT::urlsafeB64Decode(Str::before($jwt, '.')));
20
            if (isset($header->alg) && $header->alg !== config('jwtauthroles.alg')) {
21
                throw AuthException::auth(422, 'Invalid algorithm');
22
            }
23
24
            return $header->kid ?? null;
25
        } else {
26
            throw AuthException::auth(422, 'Malformed JWT');
27
        }
28
    }
29
30
    private static function getClaims(string $jwt): ?object
31
    {
32
        if (Str::is('*.*.*', $jwt)) {
33
            $claims = explode('.', $jwt);
34
            $claims = JWT::jsonDecode(JWT::urlsafeB64Decode($claims[1]));
35
36
            return $claims ?? null;
37
        } else {
38
            throw AuthException::auth(422, 'Malformed JWT');
39
        }
40
    }
41
42
    /**
43
     * @param object $jwk
44
     * @return bool|string|null
45
     */
46
    private static function jwkToPem(object $jwk)
47
    {
48
        if (isset($jwk->e) && isset($jwk->n)) {
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();
56
        }
57
        throw AuthException::auth(500, 'Malformed jwk');
58
    }
59
60
    /**
61
     * @param string $kid
62
     * @param string $uri
63
     * @return bool|string|null
64
     */
65
    private static function getJwk(string $kid, string $uri)
66
    {
67
        $response = Http::get($uri);
68
        $json = $response->getBody();
69
        if ($json) {
70
            $jwks = json_decode($json, false);
71
            if ($jwks && isset($jwks->keys) && is_array($jwks->keys)) {
72
                foreach ($jwks->keys as $jwk) {
73
                    if ($jwk->kid === $kid) {
74
                        return self::jwkToPem($jwk);
75
                    }
76
                }
77
            }
78
        }
79
        throw AuthException::auth(404, 'jwks endpoint not found');
80
    }
81
82
    private static function getPem(string $kid, string $uri): ?string
83
    {
84
        $response = Http::get($uri);
85
        $json = $response->getBody();
86
        if ($json) {
87
            $pems = json_decode($json, false);
88
            if ($pems && isset($pems->publicKeys) && is_object($pems->publicKeys)) {
89
                foreach ($pems->publicKeys as $key=>$pem) {
90
                    if ($key === $kid) {
91
                        return $pem;
92
                    }
93
                }
94
            }
95
        }
96
        throw AuthException::auth(404, 'pem endpoint not found');
97
    }
98
99
    private static function verifyToken(string $jwt, string $uri, bool $jwk = false): object
100
    {
101
        $kid = self::getKid($jwt);
102
        if (! $kid) {
103
            throw AuthException::auth(422, 'Malformed JWT');
104
        }
105
        if (config('jwtauthroles.cache.enabled')) {
106
            if (config('jwtauthroles.cache.type') === 'database') {
107
                $row = JwtKey::where('kid', $kid)
108
                    ->orderBy('created_at', 'desc')
109
                    ->first('key');
110
            }
111
        }
112
113
        $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...
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...
114
            ?? $jwk
115
                ? self::getJwk($kid, $uri)
116
                : self::getPem($kid, $uri);
117
118
        if (! isset($publicKey) || ! $publicKey) {
119
            throw AuthException::auth(500, 'Unable to validate JWT');
120
        }
121
122
        if (config('jwtauthroles.cache.enabled')) {
123
            if (config('jwtauthroles.cache.type') === 'database') {
124
                $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...
Unused Code introduced by
The assignment to $row is dead and can be removed.
Loading history...
125
            }
126
        }
127
128
        return JWT::decode($jwt, $publicKey, [config('jwtauthroles.alg')]);
129
    }
130
131
    /** @return mixed */
132
    public static function authUser(object $request)
133
    {
134
        $jwt = $request->bearerToken();
135
136
        $uri = config('jwtauthroles.useJwk')
137
            ? config('jwtauthroles.jwkUri')
138
            : config('jwtauthroles.pemUri');
139
140
        if (! config('jwtauthroles.validateJwt')) {
141
            $claims = self::getClaims($jwt);
142
        } else {
143
            $claims = self::verifyToken($jwt, $uri, config('jwtauthroles.useJwk'));
144
        }
145
146
        if(config('jwtauthroles.useDB')) {
147
            if (config('jwtauthroles.autoCreateUser')) {
148
                $user = JwtUser::firstOrNew([config('jwtauthroles.userId') => $claims->sub]);
149
                $user[config('jwtauthroles.userId')] = $claims->sub;
150
                $user->roles = json_encode($claims->roles);
151
                $user->claims = json_encode($claims);
152
                $user->save();
153
            } else {
154
                $user = JwtUser::where(config('jwtauthroles.userId'), '=', $claims->sub)->firstOrFail();
155
                $user->roles = json_encode($claims->roles);
156
                $user->claims = json_encode($claims);
157
                $user->save();
158
            }
159
        } else {
160
            $user = new JwtUser();
161
            $user->uuid = $claims->sub;
162
            $user->roles = $claims->roles;
163
            $user->claims = $claims;
164
        }
165
166
        return $user;
167
    }
168
}
169