TokenConfirmation::getToken()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 6
c 1
b 0
f 0
dl 0
loc 12
ccs 7
cts 7
cp 1
rs 10
cc 2
nc 2
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Jasny\Auth\Confirmation;
6
7
use BadMethodCallException;
8
use Closure;
9
use DateTime;
10
use DateTimeInterface;
11
use InvalidArgumentException;
12
use Jasny\Auth\Storage\TokenStorageInterface;
13
use Jasny\Auth\StorageInterface as Storage;
14
use Jasny\Auth\UserInterface as User;
15
use Jasny\Immutable;
16
use Psr\Log\LoggerInterface as Logger;
17
use Psr\Log\NullLogger;
18
19
/**
20
 * Generate random confirmation tokens.
21
 * The random token needs to be stored to the database.
22
 */
23
class TokenConfirmation implements ConfirmationInterface
24
{
25
    use Immutable\With;
26
27
    /** @var int<1, max> */
28
    protected int $numberOfBytes;
29
30
    protected Closure $encode;
31
32
    protected TokenStorageInterface $storage;
33
    protected Logger $logger;
34
35
    protected string $subject;
36
37
    /**
38
     * Class constructor.
39
     *
40
     * @param int           $numberOfBytes  Number of bytes of the random string.
41
     * @param callable|null $encode         Method to encode random string.
42
     */
43 8
    public function __construct(int $numberOfBytes = 16, ?callable $encode = null)
44
    {
45 8
        if ($numberOfBytes < 1) {
46
            throw new InvalidArgumentException("Number of bytes must be at least 1");
47
        }
48
49 8
        $this->numberOfBytes = $numberOfBytes;
50 8
        $this->encode = ($encode ?? [__CLASS__, 'encode'])(...);
51
52 8
        $this->logger = new NullLogger();
53
    }
54
55
    /**
56
     * Get copy with storage service.
57
     *
58
     * @param Storage $storage
59
     * @return static
60
     */
61 8
    public function withStorage(Storage $storage): static
62
    {
63 8
        if (!$storage instanceof TokenStorageInterface) {
64 1
            throw new InvalidArgumentException("Storage object needs to implement " . TokenStorageInterface::class);
65
        }
66
67 8
        return $this->withProperty('storage', $storage);
68
    }
69
70
    /**
71
     * @inheritDoc
72
     */
73 8
    public function withLogger(Logger $logger): static
74
    {
75 8
        return $this->withProperty('logger', $logger);
76
    }
77
78
    /**
79
     * @inheritDoc
80
     */
81 8
    public function withSubject(string $subject): static
82
    {
83 8
        return $this->withProperty('subject', $subject);
84
    }
85
86
    /**
87
     * @inheritDoc
88
     */
89 2
    public function getToken(User $user, DateTimeInterface $expire): string
90
    {
91 2
        if (!isset($this->storage)) {
92 1
            throw new BadMethodCallException("Storage is not set");
93
        }
94
95 1
        $rawToken = random_bytes($this->numberOfBytes);
96 1
        $token = ($this->encode)($rawToken);
97
98 1
        $this->storage->saveToken($this->subject, $token, $user, $expire);
99
100 1
        return $token;
101
    }
102
103
    /**
104
     * @inheritDoc
105
     */
106 5
    public function from(string $token): User
107
    {
108 5
        if (!isset($this->storage)) {
109 1
            throw new BadMethodCallException("Storage is not set");
110
        }
111
112 4
        $info = $this->storage->fetchToken($this->subject, $token);
113
114 4
        if ($info === null) {
115 1
            $this->logger->debug('Unknown confirmation token', ['token' => $token]);
116 1
            throw new InvalidTokenException("Token has been revoked");
117
        }
118
119 3
        ['uid' => $uid, 'expire' => $expire] = $info;
120
121 3
        if ($expire <= new DateTime()) {
122 1
            $this->logger->debug('Expired confirmation token', ['token' => $token, 'uid' => $uid]);
123 1
            throw new InvalidTokenException("Token is expired");
124
        }
125
126 2
        $user = $this->storage->fetchUserById($uid);
127
128 2
        if ($user === null) {
129 1
            $this->logger->debug('Invalid confirmation token: user not available', ['token' => $token, 'uid' => $uid]);
130 1
            throw new InvalidTokenException("Token has been revoked");
131
        }
132
133 1
        return $user;
134
    }
135
136
    /**
137
     * Encode the raw token to an alphanumeric string.
138
     */
139 1
    protected static function encode(string $rawToken): string
140
    {
141 1
        return base_convert(bin2hex($rawToken), 16, 36);
142
    }
143
}
144