• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

NIT-Administrative-Systems / SysDev-laravel-soa / 23020429101

12 Mar 2026 07:38PM UTC coverage: 42.393% (+2.6%) from 39.779%
23020429101

push

github

web-flow
JWT Dependency Bump & Addressing Breaking Changes (#188)

Co-authored-by: Danny Foster <danny@foster.sh>

38 of 70 new or added lines in 4 files covered. (54.29%)

326 of 769 relevant lines covered (42.39%)

18.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

82.61
/src/Auth/OAuth2/Key/KeyToConstraintAdapter.php
1
<?php
2

3
namespace Northwestern\SysDev\SOA\Auth\OAuth2\Key;
4

5
use Firebase\JWT\JWK;
6
use Firebase\JWT\Key;
7
use GuzzleHttp\Client;
8
use Illuminate\Support\Facades\Cache;
9
use Laravel\Socialite\Two\InvalidStateException;
10
use Lcobucci\JWT\Signer;
11
use Lcobucci\JWT\Signer\Key\InMemory;
12

13
use Lcobucci\JWT\Validation\Constraint;
14
use Lcobucci\JWT\Validation\Validator;
15

16
class KeyToConstraintAdapter
17
{
18
    public function configToConstraints(string $keysUrl): KeyConstraintContainer
19
    {
20
        $rawData = $this->loadKeys($keysUrl);
16✔
21
        $keyset = $this->parsePrivateKeys($rawData);
16✔
22

23
        return $this->keysToConstraint($keyset);
12✔
24
    }
25

26
    /**
27
     * @return Key[]
28
     */
29
    private function parsePrivateKeys(array $data): array
30
    {
31
        /**
32
         * This is kind of jank, but the `alg` claim in the JWK is not required by the spec, so Microsoft has opted
33
         * not to include it.
34
         *
35
         * As of v6, the JWT library requires either the alg to be provided -or- a default given, to mitigate
36
         * CVE-2021-46743, a key type confusion attack. The CVE is probably broadly applicable to any implementation
37
         * dealing with these keys missing their `alg` claims.
38
         *
39
         * If Microsoft updates in the future, they will hopefully start providing the `alg` claim on the new keys in
40
         * the keyring. In that case, this will continue to work just fine, since the `alg` claim has priority over
41
         * this default.
42
         *
43
         * @see https://github.com/firebase/php-jwt/issues/498
44
         * @see https://github.com/advisories/GHSA-8xf4-w7qw-pjjw
45
         * @see https://github.com/firebase/php-jwt/issues/351
46
         */
47
        $defaultAlgorithm = 'RS256';
16✔
48

49
        return JWK::parseKeySet($data, $defaultAlgorithm);
16✔
50
    }
51

52
    /**
53
     * Generates key constraints from a JWKS endpoint for use with {@see Validator}, to ensure it's signed by a valid
54
     * key.
55
     *
56
     * The keys will be cached for five minutes instead of re-fetching them every request.
57
     *
58
     * The returned constraint MUST be used immediately. It MUST NOT be serialized and stored. The JWKS key revocation
59
     * mechanism is "delete the key from the JSON file", so old constraints CANNOT be kept around and reused.
60
     *
61
     * The constraint WILL expire and return invalid after the `$secondsForValidConstraint` period.
62
     *
63
     * @param Key[] $keySet
64
     */
65
    private function keysToConstraint(array $keySet, int $secondsForValidConstraint = 60): KeyConstraintContainer
66
    {
67
        $constraints = [];
12✔
68
        $failedKeys = [];
12✔
69

70
        foreach ($keySet as $key) {
12✔
71
            try {
72
                $signer = $this->jwksAlgorithmToSignerImplementationFactory($key);
12✔
73
            } catch (InvalidStateException $e) {
8✔
74
                $failedKeys[] = new FailedKey('Algorithm unsupported', $key, $e);
8✔
75
                continue;
8✔
76
            }
77

78
            // The valid-until was meant to be a key rotation mechanism, allowing overlap with a built-in drop-dead date.
79
            // We're loading the keys from JWKS instead of supplying them in a config file for the app, so this isn't
80
            // applicable. The constraint should be used IMMEDIATELY, so it will be valid for the next minute only.
81
            $validUntil = now()->addSeconds($secondsForValidConstraint);
8✔
82

83
            $constraints[] = new Constraint\SignedWithUntilDate(
8✔
84
                signer: $signer,
8✔
85
                key: $this->unpackKeyMaterial($key),
8✔
86
                validUntil: $validUntil->toDateTimeImmutable(),
8✔
87
            );
8✔
88
        }
89

90
        $constraint = null;
12✔
91
        if (count($constraints) > 0) {
12✔
92
            $constraint = new Constraint\SignedWithOneInSet(...$constraints);
8✔
93
        }
94

95
        return new KeyConstraintContainer($constraint, $failedKeys);
12✔
96
    }
97

98
    public function unpackKeyMaterial(Key $key): InMemory
99
    {
100
        if (is_string($key->getKeyMaterial())) {
20✔
101
            return InMemory::plainText($key->getKeyMaterial());
4✔
102
        }
103

104
        $keyMaterial = $key->getKeyMaterial();
16✔
105
        if ($keyMaterial instanceof \OpenSSLCertificate) {
16✔
106
            $keyMaterial = openssl_pkey_get_public($keyMaterial);
4✔
107
        }
108

109
        $details = openssl_pkey_get_details($keyMaterial);
16✔
110
        return InMemory::plainText($details['key']);
16✔
111
    }
112

113
    private function jwksAlgorithmToSignerImplementationFactory(Key $key): Signer
114
    {
115
        return match ($key->getAlgorithm()) {
12✔
116
            'RS256' => new Signer\Rsa\Sha256(),
8✔
NEW
117
            'RS384' => new Signer\Rsa\Sha384(),
×
NEW
118
            'RS512' => new Signer\Rsa\Sha512(),
×
NEW
119
            'ES256' => new Signer\Ecdsa\Sha256(),
×
NEW
120
            'ES384' => new Signer\Ecdsa\Sha384(),
×
NEW
121
            'ES512' => new Signer\Ecdsa\Sha512(),
×
NEW
122
            'EdDSA' => new Signer\Eddsa(),
×
123
            default => throw new InvalidStateException("Unsupported signed algorithm type {$key->getAlgorithm()}"),
12✔
124
        };
12✔
125
    }
126

127
    private function loadKeys(string $keysUrl): array
128
    {
129
        $cacheKeyHash = $this->keysetCacheKey($keysUrl);
16✔
130

131
        return Cache::remember($cacheKeyHash, 5 * 60, function () use ($keysUrl) {
16✔
NEW
132
            $response = (new Client())->get($keysUrl);
×
133

NEW
134
            return json_decode($response->getBody()->getContents(), true);
×
135
        });
16✔
136
    }
137

138
    private function keysetCacheKey(string $keysUrl): string
139
    {
140
        $hash = hash('sha256', $keysUrl);
16✔
141

142
        return "socialite:Azure-JWKSet:{$hash}";
16✔
143
    }
144
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc