1
<?php
2

3
declare(strict_types=1);
4

5
namespace Kreait\Firebase\Auth;
6

7
use DateTimeImmutable;
8
use Firebase\Auth\Token\Domain\Verifier;
9
use Firebase\Auth\Token\Exception\ExpiredToken;
10
use Firebase\Auth\Token\Exception\InvalidSignature;
11
use Firebase\Auth\Token\Exception\InvalidToken;
12
use Firebase\Auth\Token\Exception\IssuedInTheFuture;
13
use Firebase\Auth\Token\Exception\UnknownKey;
14
use Kreait\Clock;
15
use Kreait\Firebase\Exception\InvalidArgumentException;
16
use Lcobucci\JWT\Parser;
17
use Lcobucci\JWT\Token;
18
use Throwable;
19

20
final class IdTokenVerifier implements Verifier
21
{
22
    /** @var Verifier */
23
    private $verifier;
24

25
    /** @var Clock */
26
    private $clock;
27

28
    /** @var int */
29
    private $leewayInSeconds = 0;
30

31 12
    public function __construct(Verifier $verifier, Clock $clock)
32
    {
33 12
        $this->verifier = $verifier;
34 12
        $this->clock = $clock;
35
    }
36

37 12
    public function withLeewayInSeconds(int $leewayInSeconds): self
38
    {
39 12
        $verifier = new self($this->verifier, $this->clock);
40 12
        $verifier->leewayInSeconds = $leewayInSeconds;
41

42 12
        return $verifier;
43
    }
44

45 12
    public function verifyIdToken($token): Token
46
    {
47
        // We get $now now™ so that it doesn't change while processing
48 12
        $now = $this->clock->now();
49

50 12
        $token = $this->ensureToken($token);
51

52
        try {
53 12
            $this->verifier->verifyIdToken($token);
54

55
            // We're using getClaim() instead of hasClaim() to also check for an empty value
56 12
            if (!($token->getClaim('sub', false))) {
57 9
                throw new InvalidToken($token, 'The token has no "sub" claim');
58
            }
59

60 12
            return $token;
61 9
        } catch (UnknownKey $e) {
62 9
            throw $e;
63 9
        } catch (InvalidSignature $e) {
64 9
            throw $e;
65 9
        } catch (ExpiredToken $e) {
66
            // Re-check expiry with the clock
67 9
            if ($this->isNotExpired($token, $now)) {
68 9
                return $token;
69
            }
70

71 9
            throw $e;
72 9
        } catch (IssuedInTheFuture $e) {
73
            // Re-check expiry with the clock
74 9
            if ($this->isIssuedInThePast($token, $now)) {
75 9
                return $token;
76
            }
77

78 9
            throw $e;
79 9
        } catch (InvalidToken $e) {
80 9
            $isAuthTimeProblem = \mb_stripos($e->getMessage(), 'authentication time') !== false;
81 9
            if ($isAuthTimeProblem && $this->isAuthenticatedInThePast($token, $now)) {
82 9
                return $token;
83
            }
84

85 9
            throw $e;
86 9
        } catch (Throwable $e) {
87 9
            throw new InvalidToken($token, $e->getMessage(), $e->getCode(), $e);
88
        }
89
    }
90

91 9
    private function isNotExpired(Token $token, DateTimeImmutable $now): bool
92
    {
93 9
        $claim = $token->getClaim('exp');
94

95
        // We add another second to account for possible microseconds that could be in $now, but not in $expiresAt
96 9
        $check = $now->modify('-'.($this->leewayInSeconds + 1).' seconds');
97 9
        $expiresAt = $now->setTimestamp((int) $claim);
98

99 9
        return $expiresAt > $check;
100
    }
101

102 9
    private function isIssuedInThePast(Token $token, DateTimeImmutable $now): bool
103
    {
104 9
        $claim = $token->getClaim('iat');
105

106
        // We add another second to account for possible microseconds that could be in $now, but not in $issuedAt
107 9
        $check = $now->modify('+'.($this->leewayInSeconds + 1).' seconds');
108 9
        $issuedAt = $now->setTimestamp((int) $claim);
109

110 9
        return $issuedAt < $check;
111
    }
112

113 9
    private function isAuthenticatedInThePast(Token $token, DateTimeImmutable $now): bool
114
    {
115 9
        $claim = $token->getClaim('auth_time');
116

117
        // We add another second to account for possible microseconds that could be in $now, but not in $authenticatedAt
118 9
        $check = $now->modify('+'.($this->leewayInSeconds + 1).' seconds');
119 9
        $authenticatedAt = $now->setTimestamp((int) $claim);
120

121 9
        return $authenticatedAt < $check;
122
    }
123

124
    /**
125
     * @param Token|object|string $token
126
     */
127 12
    private function ensureToken($token): Token
128
    {
129 12
        if ($token instanceof Token) {
130 9
            return $token;
131
        }
132

133 12
        if (\is_object($token) && !\method_exists($token, '__toString')) {
134 9
            throw new InvalidArgumentException('The given token is an object and cannot be cast to a string');
135
        }
136

137
        try {
138 12
            return (new Parser())->parse((string) $token);
139 9
        } catch (Throwable $e) {
140 9
            throw new InvalidArgumentException('The given token could not be parsed: '.$e->getMessage());
141
        }
142
    }
143
}

Read our documentation on viewing source code .

Loading