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

daycry / auth / 26937880755

04 Jun 2026 07:38AM UTC coverage: 75.983% (+4.4%) from 71.569%
26937880755

push

github

web-flow
Merge pull request #56 from daycry/development

feat

613 of 719 new or added lines in 42 files covered. (85.26%)

3 existing lines in 3 files now uncovered.

5179 of 6816 relevant lines covered (75.98%)

69.66 hits per line

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

60.71
/src/Controllers/WebAuthnController.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of Daycry Auth.
7
 *
8
 * (c) Daycry <daycry9@proton.me>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace Daycry\Auth\Controllers;
15

16
use CodeIgniter\HTTP\ResponseInterface;
17
use Daycry\Auth\Exceptions\WebAuthnDuplicateCredentialException;
18
use Daycry\Auth\Exceptions\WebAuthnException;
19
use Daycry\Auth\Libraries\WebAuthn\WebAuthnManager;
20

21
/**
22
 * JSON endpoints for the WebAuthn ceremonies. All methods 404 when the feature
23
 * is globally disabled (defense in depth on top of Auth::routes() gating).
24
 */
25
class WebAuthnController extends BaseAuthController
26
{
27
    /**
28
     * This controller exposes only JSON ceremony endpoints; it does not use
29
     * the form-validation flow that BaseAuthController otherwise requires.
30
     */
NEW
31
    protected function getValidationRules(): array
×
32
    {
NEW
33
        return [];
×
34
    }
35

36
    private function enabled(): bool
4✔
37
    {
38
        return (bool) (setting('AuthSecurity.webauthnEnabled') ?? false);
4✔
39
    }
40

41
    private function manager(): WebAuthnManager
2✔
42
    {
43
        return service('webAuthnManager');
2✔
44
    }
45

46
    private function error(string $message, int $status, string $code = 'error'): ResponseInterface
3✔
47
    {
48
        return $this->response->setStatusCode($status)->setJSON([
3✔
49
            'status'  => 'error',
3✔
50
            'error'   => $code,
3✔
51
            'message' => $message,
3✔
52
            'token'   => csrf_hash(),
3✔
53
        ]);
3✔
54
    }
55

56
    /**
57
     * Adds the freshly-rotated CSRF token to a JSON payload so the reference
58
     * ceremony JS can carry it into the follow-up request (CI4 regenerates the
59
     * token per request when csrf.regenerate is enabled). Harmless for the
60
     * WebAuthn option objects: the browser ignores unknown top-level keys.
61
     *
62
     * @param array<string, mixed> $payload
63
     *
64
     * @return array<string, mixed>
65
     */
66
    private function withToken(array $payload): array
2✔
67
    {
68
        $payload['token'] = csrf_hash();
2✔
69

70
        return $payload;
2✔
71
    }
72

73
    public function registerOptions(): ResponseInterface
3✔
74
    {
75
        if (! $this->enabled()) {
3✔
NEW
76
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
×
77
        }
78
        if (! auth()->loggedIn()) {
3✔
79
            return $this->error(lang('Auth.notLoggedIn'), 403, 'forbidden');
1✔
80
        }
81

82
        try {
83
            $options = $this->manager()->startRegistration(auth()->user(), $this->request->getPost('name') ?: null);
2✔
NEW
84
        } catch (WebAuthnException $e) {
×
NEW
85
            return $this->error($e->getMessage(), 409, 'conflict');
×
86
        }
87

88
        return $this->response->setJSON($this->withToken($options));
2✔
89
    }
90

91
    public function registerVerify(): ResponseInterface
2✔
92
    {
93
        if (! $this->enabled()) {
2✔
NEW
94
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
×
95
        }
96
        if (! auth()->loggedIn()) {
2✔
NEW
97
            return $this->error(lang('Auth.notLoggedIn'), 403, 'forbidden');
×
98
        }
99

100
        $json = $this->credentialJson();
2✔
101
        if ($json === null) {
2✔
NEW
102
            return $this->error(lang('Auth.webauthnVerificationFailed'), 400, 'bad_request');
×
103
        }
104

105
        try {
106
            $entity = $this->manager()->finishRegistration(auth()->user(), $json);
2✔
107
        } catch (WebAuthnDuplicateCredentialException $e) {
1✔
108
            return $this->error($e->getMessage(), 409, 'conflict');
1✔
NEW
109
        } catch (WebAuthnException $e) {
×
NEW
110
            return $this->error($e->getMessage(), 422, 'unprocessable');
×
111
        }
112

113
        return $this->response->setStatusCode(201)->setJSON($this->withToken([
2✔
114
            'status'     => 'ok',
2✔
115
            'credential' => ['uuid' => $entity->uuid, 'name' => $entity->name],
2✔
116
        ]));
2✔
117
    }
118

119
    public function loginOptions(): ResponseInterface
2✔
120
    {
121
        if (! $this->enabled()) {
2✔
122
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
1✔
123
        }
124

125
        $options = $this->manager()->startLogin($this->request->getPost('email') ?: null);
1✔
126

127
        return $this->response->setJSON($this->withToken($options));
1✔
128
    }
129

130
    public function loginVerify(): ResponseInterface
1✔
131
    {
132
        if (! $this->enabled()) {
1✔
NEW
133
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
×
134
        }
135

136
        $json = $this->credentialJson();
1✔
137
        if ($json === null) {
1✔
NEW
138
            return $this->error(lang('Auth.webauthnVerificationFailed'), 400, 'bad_request');
×
139
        }
140

141
        try {
142
            $user = $this->manager()->finishLogin($json);
1✔
NEW
143
        } catch (WebAuthnException $e) {
×
NEW
144
            return $this->error($e->getMessage(), 422, 'unprocessable');
×
145
        }
146

147
        // A verified passkey (with user verification) is multi-factor: complete
148
        // the session directly without re-running the 'login' Action pipeline.
149
        auth()->login($user, false);
1✔
150

151
        return $this->response->setJSON($this->withToken([
1✔
152
            'status'   => 'ok',
1✔
153
            'redirect' => config('Auth')->loginRedirect(),
1✔
154
        ]));
1✔
155
    }
156

NEW
157
    public function twoFactorOptions(): ResponseInterface
×
158
    {
NEW
159
        if (! $this->enabled()) {
×
NEW
160
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
×
161
        }
162

NEW
163
        $pending = auth()->getPendingUser();
×
NEW
164
        if ($pending === null) {
×
NEW
165
            return $this->error(lang('Auth.webauthnVerificationFailed'), 422, 'unprocessable');
×
166
        }
167

NEW
168
        return $this->response->setJSON($this->withToken($this->manager()->startTwoFactor($pending)));
×
169
    }
170

NEW
171
    public function deleteCredential(string $uuid): ResponseInterface
×
172
    {
NEW
173
        if (! $this->enabled()) {
×
NEW
174
            return $this->error(lang('Auth.webauthnDisabled'), 404, 'disabled');
×
175
        }
NEW
176
        if (! auth()->loggedIn()) {
×
NEW
177
            return $this->error(lang('Auth.notLoggedIn'), 403, 'forbidden');
×
178
        }
179

NEW
180
        $ok = auth()->user()->revokeWebAuthnCredential($uuid);
×
181

NEW
182
        return $this->response->setStatusCode($ok ? 200 : 404)->setJSON($this->withToken(['status' => $ok ? 'ok' : 'not_found']));
×
183
    }
184

185
    /**
186
     * Extracts the browser PublicKeyCredential JSON from the request body
187
     * (accepts a `credential` field or the raw body).
188
     */
189
    private function credentialJson(): ?string
2✔
190
    {
191
        $body = $this->request->getJSON(true);
2✔
192
        if (is_array($body) && isset($body['credential'])) {
2✔
193
            return json_encode($body['credential'], JSON_THROW_ON_ERROR);
2✔
194
        }
NEW
195
        $posted = $this->request->getPost('credential');
×
NEW
196
        if (is_string($posted) && $posted !== '') {
×
NEW
197
            return $posted;
×
198
        }
NEW
199
        $raw = (string) $this->request->getBody();
×
200

NEW
201
        return $raw !== '' ? $raw : null;
×
202
    }
203
}
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