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

daycry / auth / 25552752016

08 May 2026 11:20AM UTC coverage: 71.592% (+13.0%) from 58.608%
25552752016

push

github

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

Add Laravel-parity authentication features: Gates, Password Confirmation, Basic Auth

198 of 252 new or added lines in 18 files covered. (78.57%)

1 existing line in 1 file now uncovered.

4453 of 6220 relevant lines covered (71.59%)

62.44 hits per line

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

95.35
/src/Filters/BasicAuthFilter.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\Filters;
15

16
use CodeIgniter\Filters\FilterInterface;
17
use CodeIgniter\HTTP\RequestInterface;
18
use CodeIgniter\HTTP\ResponseInterface;
19
use Daycry\Auth\Authentication\Passwords;
20
use Daycry\Auth\Models\UserModel;
21

22
/**
23
 * HTTP Basic authentication filter (RFC 7617).
24
 *
25
 * Reads the `Authorization: Basic base64(user:pass)` request header and
26
 * authenticates the user against the configured user provider. Useful for
27
 * machine-to-machine endpoints (cron, health checks, internal tooling)
28
 * where managing tokens or sessions is unnecessary overhead.
29
 *
30
 * Behaviour:
31
 *   - On success: logs the user in via the `session` authenticator so
32
 *     `auth()->user()` works for the rest of the request lifecycle.
33
 *   - On failure (missing / malformed header, unknown user, wrong
34
 *     password): returns 401 with `WWW-Authenticate: Basic realm="..."`
35
 *     so browsers prompt for credentials and clients see the expected
36
 *     challenge.
37
 *
38
 * Use the `once` argument (`basic-auth:once`) when you do NOT want to
39
 * persist the auth into the session — useful for stateless API endpoints
40
 * that should re-verify credentials on every request.
41
 *
42
 * The realm string can be overridden in `app/Config/Auth.php`:
43
 *
44
 *     public string $basicAuthRealm = 'My App API';
45
 */
46
class BasicAuthFilter implements FilterInterface
47
{
48
    /**
49
     * @param array|null $arguments ['once'] disables session login.
50
     */
51
    public function before(RequestInterface $request, $arguments = null)
9✔
52
    {
53
        $header = $request->getHeaderLine('Authorization');
9✔
54

55
        $credentials = $this->extractCredentials($header);
9✔
56

57
        if ($credentials === null) {
9✔
58
            return $this->challenge();
5✔
59
        }
60

61
        [$identifier, $password] = $credentials;
4✔
62

63
        $user = $this->resolveUser($identifier);
4✔
64

65
        if ($user === null) {
4✔
66
            return $this->challenge();
1✔
67
        }
68

69
        /** @var Passwords $passwords */
70
        $passwords = service('passwords');
3✔
71

72
        if (! $passwords->verify($password, $user->getPasswordHash())) {
3✔
73
            return $this->challenge();
1✔
74
        }
75

76
        // Persist into the session unless the caller asked for stateless auth.
77
        $stateless = $arguments !== null && in_array('once', $arguments, true);
2✔
78

79
        if (! $stateless) {
2✔
80
            auth('session')->login($user);
2✔
81
        } else {
82
            // Stateless: still expose $user via the session authenticator's
83
            // in-memory state so $this->request->user() / auth()->user()
84
            // work for downstream code in this request.
NEW
85
            auth('session')->loginById($user->id);
×
86
        }
87

88
        return null;
2✔
89
    }
90

91
    /**
92
     * Extracts and decodes the `user:password` pair from a `Basic` header.
93
     * Returns null when the header is missing, the scheme is wrong, or the
94
     * payload is not valid base64 / does not contain a colon.
95
     *
96
     * @return array{0: string, 1: string}|null
97
     */
98
    private function extractCredentials(string $header): ?array
9✔
99
    {
100
        if ($header === '' || ! str_starts_with(strtolower($header), strtolower('Basic '))) {
9✔
101
            return null;
3✔
102
        }
103

104
        $payload = trim(substr($header, 6));
6✔
105
        $decoded = base64_decode($payload, true);
6✔
106

107
        if ($decoded === false || ! str_contains($decoded, ':')) {
6✔
108
            return null;
2✔
109
        }
110

111
        [$user, $pass] = explode(':', $decoded, 2);
4✔
112

113
        if ($user === '' || $pass === '') {
4✔
NEW
114
            return null;
×
115
        }
116

117
        return [$user, $pass];
4✔
118
    }
119

120
    /**
121
     * Resolves a user by email when the identifier looks like an email,
122
     * otherwise by username. Returns null when no match is found.
123
     */
124
    private function resolveUser(string $identifier)
4✔
125
    {
126
        /** @var UserModel $userModel */
127
        $userModel = model(UserModel::class);
4✔
128

129
        $field = filter_var($identifier, FILTER_VALIDATE_EMAIL) !== false
4✔
130
            ? 'email'
3✔
131
            : 'username';
1✔
132

133
        return $userModel->findByCredentials([$field => $identifier]);
4✔
134
    }
135

136
    /**
137
     * Returns a 401 response with the standard `WWW-Authenticate` challenge.
138
     */
139
    private function challenge(): ResponseInterface
7✔
140
    {
141
        $realm = (string) config('Auth')->basicAuthRealm;
7✔
142

143
        // RFC 7617 §2 — realm must be quoted; escape any embedded quotes.
144
        $realm = str_replace('"', '\\"', $realm);
7✔
145

146
        return service('response')
7✔
147
            ->setStatusCode(ResponseInterface::HTTP_UNAUTHORIZED)
7✔
148
            ->setHeader('WWW-Authenticate', 'Basic realm="' . $realm . '", charset="UTF-8"')
7✔
149
            ->setJSON(['message' => lang('Auth.badAttempt')]);
7✔
150
    }
151

152
    /**
153
     * @param array|null $arguments
154
     */
155
    public function after(RequestInterface $request, ResponseInterface $response, $arguments = null): void
2✔
156
    {
157
        // Nothing required.
158
    }
2✔
159
}
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