1
<?php
2

3
declare(strict_types=1);
4

5
namespace SilverStripe\WebAuthn;
6

7
use InvalidArgumentException;
8
use Serializable;
9
use Webauthn\AttestedCredentialData;
10
use Webauthn\PublicKeyCredentialSource;
11
use Webauthn\PublicKeyCredentialSourceRepository;
12
use Webauthn\PublicKeyCredentialUserEntity;
13

14
/**
15
 * Implements the required interface from the WebAuthn library - but it does not implement the repository pattern in the
16
 * usual way. This is expected to be stored on a DataObject for persistence. Use the
17
 * @see CredentialRepository::hasChanged() API for determining whether a DataObject this is stored upon should be
18
 * persisted.
19
 */
20
class CredentialRepository implements PublicKeyCredentialSourceRepository, Serializable
21
{
22
    /**
23
     * @var string
24
     */
25
    private $memberID;
26

27
    /**
28
     * @var array
29
     */
30
    private $credentials = [];
31

32
    /**
33
     * @var bool
34
     */
35
    private $hasChanged = false;
36

37
    /**
38
     * @param string $memberID
39
     */
40 1
    public function __construct(string $memberID)
41
    {
42 1
        $this->memberID = $memberID;
43
    }
44

45 1
    public function has(string $credentialId): bool
46
    {
47 1
        return $this->findOneByCredentialId($credentialId) !== null;
48
    }
49

50 1
    public function get(string $credentialId): AttestedCredentialData
51
    {
52 1
        $this->assertCredentialID($credentialId);
53

54 1
        return $this->findOneByCredentialId($credentialId)->getAttestedCredentialData();
55
    }
56

57 1
    public function getUserHandleFor(string $credentialId): string
58
    {
59 1
        $this->assertCredentialID($credentialId);
60

61 1
        return $this->memberID;
62
    }
63

64 1
    public function getCounterFor(string $credentialId): int
65
    {
66 1
        $this->assertCredentialID($credentialId);
67

68 1
        return (int) $this->credentials[$this->getCredentialIDRef($credentialId)]['counter'];
69
    }
70

71 1
    public function updateCounterFor(string $credentialId, int $newCounter): void
72
    {
73 1
        $this->assertCredentialID($credentialId);
74

75 1
        $this->credentials[$this->getCredentialIDRef($credentialId)]['counter'] = $newCounter;
76 1
        $this->hasChanged = true;
77
    }
78

79
    /**
80
     * Assert that the given credential ID matches a stored credential
81
     *
82
     * @param string $credentialId
83
     */
84 1
    protected function assertCredentialID(string $credentialId): void
85
    {
86 1
        if (!$this->has($credentialId)) {
87 1
            throw new InvalidArgumentException('Given credential ID does not match any stored credentials');
88
        }
89
    }
90

91 1
    public function findOneByCredentialId(string $publicKeyCredentialId): ?PublicKeyCredentialSource
92
    {
93 1
        $ref = $this->getCredentialIDRef($publicKeyCredentialId);
94 1
        if (!isset($this->credentials[$ref])) {
95 1
            return null;
96
        }
97

98 1
        return $this->credentials[$ref]['source'];
99
    }
100

101
    /**
102
     * @return PublicKeyCredentialSource[]
103
     */
104 1
    public function findAllForUserEntity(PublicKeyCredentialUserEntity $publicKeyCredentialUserEntity): array
105
    {
106
        // Only return credentials if the user entity shares the same ID.
107 1
        if ($publicKeyCredentialUserEntity->getId() !== $this->memberID) {
108 0
            return [];
109
        }
110

111 1
        return array_map(function ($credentialComposite) {
112 1
            return $credentialComposite['source'];
113 1
        }, $this->credentials);
114
    }
115

116 1
    public function saveCredentialSource(PublicKeyCredentialSource $publicKeyCredentialSource): void
117
    {
118 1
        $ref = $this->getCredentialIDRef($publicKeyCredentialSource->getPublicKeyCredentialId());
119

120 1
        if (!isset($this->credentials[$ref])) {
121 1
            $this->credentials[$ref] = [
122
                'counter' => 0,
123
            ];
124
        }
125

126 1
        $this->credentials[$ref]['source'] = $publicKeyCredentialSource;
127 1
        $this->hasChanged = true;
128
    }
129

130
    /**
131
     * Empty the store deleting all stored credentials
132
     */
133 1
    public function reset(): void
134
    {
135 1
        $this->credentials = [];
136
    }
137

138
    /**
139
     * Indicates the repository has changed and should be persisted (as this doesn't follow the actual repository
140
     * pattern and is expected to be stored on a dataobject for persistence)
141
     *
142
     * @return bool
143
     */
144 0
    public function hasChanged(): bool
145
    {
146 0
        return $this->hasChanged;
147
    }
148

149
    /**
150
     * Set the credentials in bulk (for internal use) ensuring that credential objects are initialised correctly
151
     *
152
     * @param array $credentials
153
     */
154
    protected function setCredentials(array $credentials): void
155
    {
156 1
        $this->credentials = array_map(function ($data) {
157 1
            $data['source'] = PublicKeyCredentialSource::createFromArray($data['source']);
158 1
            return $data;
159 1
        }, $credentials);
160
    }
161

162
    /**
163
     * Create a reference to be used as a key for the credentials in the array
164
     *
165
     * @param string $credentialID
166
     * @return string
167
     */
168 1
    protected function getCredentialIDRef(string $credentialID): string
169
    {
170 1
        return base64_encode($credentialID);
171
    }
172

173
    /**
174
     * Provide the credentials stored in this repository as an array
175
     *
176
     * @return array
177
     */
178 1
    public function toArray(): array
179
    {
180 1
        return $this->credentials;
181
    }
182

183
    /**
184
     * Create an instance of a repository from the given credentials
185
     *
186
     * @param array $credentials
187
     * @param string $memberID
188
     * @return CredentialRepository
189
     */
190 1
    public static function fromArray(array $credentials, string $memberID): self
191
    {
192 1
        $new = new static($memberID);
193 1
        $new->setCredentials($credentials);
194

195 1
        return $new;
196
    }
197

198 0
    public function serialize()
199
    {
200 0
        return json_encode(['credentials' => $this->toArray(), 'memberID' => $this->memberID]);
201
    }
202

203 0
    public function unserialize($serialized)
204
    {
205 0
        $raw = json_decode($serialized, true);
206

207 0
        $this->memberID = $raw['memberID'];
208 0
        $this->setCredentials($raw['credentials']);
209
    }
210
}

Read our documentation on viewing source code .

Loading