IdTokenValidator   A
last analyzed

Complexity

Total Complexity 19

Size/Duplication

Total Lines 110
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 19
eloc 41
c 1
b 0
f 0
dl 0
loc 110
ccs 43
cts 43
cp 1
rs 10

4 Methods

Rating   Name   Duplication   Size   Complexity  
C validate() 0 55 14
A onToken() 0 3 1
A onAuthorization() 0 9 2
A __construct() 0 3 2
1
<?php
2
3
namespace Parroauth2\Client\OpenID\Extension;
4
5
use Jose\Component\Checker\AudienceChecker;
6
use Jose\Component\Checker\ClaimCheckerManager;
7
use Jose\Component\Checker\ExpirationTimeChecker;
8
use Jose\Component\Checker\InvalidClaimException;
9
use Jose\Component\Checker\IssuedAtChecker;
10
use Parroauth2\Client\EndPoint\Authorization\AuthorizationEndPoint;
11
use Parroauth2\Client\EndPoint\EndPointTransformerTrait;
12
use Parroauth2\Client\EndPoint\Token\TokenEndPoint;
13
use Parroauth2\Client\EndPoint\Token\TokenResponse;
14
use Parroauth2\Client\Extension\AbstractEndPointTransformerExtension;
15
use Parroauth2\Client\OpenID\EndPoint\AuthorizationEndPoint as OpenIdAuthorizationEndPoint;
16
use Parroauth2\Client\OpenID\EndPoint\Token\TokenResponse as OpenIdTokenResponse;
17
use Parroauth2\Client\OpenID\IdToken\AccessTokenHash;
18
19
/**
20
 * Perform validation on the returned ID Token
21
 *
22
 * Client options :
23
 * - id_token_required (bool) Does the ID Token is required ? Default to false
24
 * - id_token_max_iat_interval (int) The max time interval (in seconds) for the ID Token issued at time. Default to 30
25
 *
26
 * @see https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
27
 */
28
final class IdTokenValidator extends AbstractEndPointTransformerExtension
29
{
30
    use EndPointTransformerTrait;
31
32
    /**
33
     * @var AccessTokenHash
34
     */
35
    private $accessTokenHash;
36
37
38
    /**
39
     * IdTokenValidator constructor.
40
     *
41
     * @param AccessTokenHash|null $accessTokenHash
42
     */
43 15
    public function __construct(?AccessTokenHash $accessTokenHash = null)
44
    {
45 15
        $this->accessTokenHash = $accessTokenHash ?: new AccessTokenHash();
46 15
    }
47
48
    /**
49
     * {@inheritdoc}
50
     */
51 3
    public function onAuthorization(AuthorizationEndPoint $endPoint): AuthorizationEndPoint
52
    {
53 3
        if ($endPoint instanceof OpenIdAuthorizationEndPoint) {
54 3
            $endPoint = $endPoint->nonce();
55
56 3
            $this->client()->storage()->store('nonce', $endPoint->get('nonce'));
57
        }
58
59 3
        return $endPoint;
60
    }
61
62
    /**
63
     * {@inheritdoc}
64
     */
65 2
    public function onToken(TokenEndPoint $endPoint): TokenEndPoint
66
    {
67 2
        return $endPoint->onResponse([$this, 'validate']);
68
    }
69
70
    /**
71
     * Validate the ID Token claims
72
     *
73
     * @param TokenResponse $response
74
     *
75
     * @throws \Jose\Component\Checker\InvalidClaimException
76
     * @throws \Jose\Component\Checker\MissingMandatoryClaimException
77
     * @throws \InvalidArgumentException
78
     *
79
     * @internal
80
     *
81
     * @todo throw only parroauth2 exception
82
     */
83 14
    public function validate(TokenResponse $response): void
84
    {
85 14
        if (!$response instanceof OpenIdTokenResponse || !$response->idToken()) {
86 2
            if ($this->client()->clientConfig()->option('id_token_required', false)) {
87 1
                throw new \InvalidArgumentException('ID Token is required on the token response');
88
            }
89
90 1
            return;
91
        }
92
93 12
        $idToken = $response->idToken();
94
95 12
        $checker = new ClaimCheckerManager([
96 12
            new IssuedAtChecker(),
97 12
            new ExpirationTimeChecker(),
98 12
            new AudienceChecker($this->client()->clientId()),
99
        ]);
100
101 12
        $checker->check($idToken->claims(), ['iss', 'sub', 'aud', 'exp', 'iat']);
102
103 9
        $client = $this->client();
104
105 9
        if ($idToken->issuer() !== $client->provider()->issuer()) {
106 1
            throw new InvalidClaimException('The issuer is invalid', 'iss', $idToken->issuer());
107
        }
108
109 8
        if (is_array($idToken->audience()) && count($idToken->audience()) > 0 && !$idToken->authorizedParty()) {
110 1
            throw new InvalidClaimException(
111 1
                'The authorized party is required when multiple audience are provided',
112 1
                'azp',
113 1
                ''
114
            );
115
        }
116
117 7
        if ($idToken->authorizedParty() && $idToken->authorizedParty() !== $client->clientId()) {
118 1
            throw new InvalidClaimException(
119 1
                'The authorized party must be identically to the current client id',
120 1
                'azp',
121 1
                $idToken->authorizedParty()
122
            );
123
        }
124
125 6
        if (time() - $idToken->issuedAt() > $client->clientConfig()->option('id_token_max_iat_interval', 30)) {
126 1
            throw new InvalidClaimException('The ID Token is issued too far in the past', 'iat', $idToken->issuedAt());
127
        }
128
129 5
        if (($nonce = $client->storage()->remove('nonce')) && !$idToken->check('nonce', $nonce)) {
130 2
            throw new InvalidClaimException('Invalid nonce', 'nonce', $idToken->nonce());
131
        }
132
133 3
        if (!$this->accessTokenHash->check($idToken, $response->accessToken())) {
134 1
            throw new InvalidClaimException(
135 1
                'Access token hash do not corresponds',
136 1
                'at_hash',
137 1
                $idToken->accessTokenHash()
138
            );
139
        }
140 2
    }
141
}
142