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

daycry / auth / 25507696361

07 May 2026 03:28PM UTC coverage: 58.608% (-6.4%) from 64.989%
25507696361

push

github

daycry
Regenerate PHPStan baseline for the new sources

Captures the 22 new files added in 72c122c and re-balances the existing
suppressions so the new model() / config() class-const fetches against
codeigniter4-standard rule sets are pinned to the new lines without
masking real issues elsewhere.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

3544 of 6047 relevant lines covered (58.61%)

47.97 hits per line

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

0.0
/src/Controllers/JwtController.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 CodeIgniter\I18n\Time;
18
use Daycry\Auth\Authentication\Authenticators\Session;
19
use Daycry\Auth\Entities\User;
20
use Daycry\Auth\Interfaces\JWTAdapterInterface;
21
use Daycry\Auth\Models\UserIdentityModel;
22
use Daycry\Auth\Models\UserModel;
23
use Daycry\Auth\Services\AuditLogger;
24
use Daycry\Auth\Validation\ValidationRules;
25

26
/**
27
 * JwtController
28
 *
29
 * Stateless JWT authentication with refresh token rotation.
30
 * Access tokens are short-lived JWTs; refresh tokens are
31
 * long-lived opaque tokens stored in the identities table.
32
 *
33
 * Routes (add to your app):
34
 *   $routes->post('auth/jwt/login',   'Daycry\Auth\Controllers\JwtController::login',   ['as' => 'jwt-login']);
35
 *   $routes->post('auth/jwt/refresh', 'Daycry\Auth\Controllers\JwtController::refresh', ['as' => 'jwt-refresh']);
36
 *   $routes->post('auth/jwt/logout',  'Daycry\Auth\Controllers\JwtController::logout',  ['as' => 'jwt-logout']);
37
 */
38
class JwtController extends BaseAuthController
39
{
40
    /**
41
     * {@inheritDoc}
42
     */
43
    protected function getValidationRules(): array
×
44
    {
45
        $rules = new ValidationRules();
×
46

47
        return $rules->getLoginRules();
×
48
    }
49

50
    /**
51
     * Authenticate with email+password.
52
     *
53
     * Returns a short-lived JWT access token and a long-lived refresh token.
54
     *
55
     * POST body: email, password
56
     * Response JSON: access_token, refresh_token, token_type
57
     */
58
    public function login(): ResponseInterface
×
59
    {
60
        $rules    = $this->getValidationRules();
×
61
        $postData = $this->request->getPost();
×
62

63
        if (! $this->validateRequest($postData, $rules)) {
×
64
            return $this->response->setStatusCode(422)->setJSON([
×
65
                'message' => $this->validator->getErrors(),
×
66
            ]);
×
67
        }
68

69
        $credentials = $this->extractLoginCredentials();
×
70

71
        /** @var Session $authenticator */
72
        $authenticator = $this->getSessionAuthenticator();
×
73
        $result        = $authenticator->check($credentials);
×
74

75
        if (! $result->isOK()) {
×
76
            return $this->response->setStatusCode(401)->setJSON([
×
77
                'message' => $result->reason(),
×
78
            ]);
×
79
        }
80

81
        $user = $result->extraInfo();
×
82

83
        return $this->response->setJSON($this->buildTokenResponse($user));
×
84
    }
85

86
    /**
87
     * Exchange a valid refresh token for a new access token and rotated refresh token.
88
     *
89
     * POST body: user_id, refresh_token
90
     * Response JSON: access_token, refresh_token, token_type
91
     */
92
    public function refresh(): ResponseInterface
×
93
    {
94
        $userId       = (int) $this->request->getPost('user_id');
×
95
        $refreshToken = (string) $this->request->getPost('refresh_token');
×
96

97
        if ($userId === 0 || $refreshToken === '') {
×
98
            return $this->response->setStatusCode(401)->setJSON([
×
99
                'message' => lang('Auth.invalidRefreshToken'),
×
100
            ]);
×
101
        }
102

103
        /** @var UserIdentityModel $identityModel */
104
        $identityModel = model(UserIdentityModel::class);
×
105
        $identity      = $identityModel->getJwtRefreshToken($userId, $refreshToken);
×
106

107
        if ($identity === null) {
×
108
            return $this->response->setStatusCode(401)->setJSON([
×
109
                'message' => lang('Auth.invalidRefreshToken'),
×
110
            ]);
×
111
        }
112

113
        // Revoke the used refresh token (rotation — one-time use)
114
        $identityModel->revokeIdentityById((int) $identity->id);
×
115

116
        /** @var UserModel $userModel */
117
        $userModel = model(UserModel::class);
×
118
        $user      = $userModel->findById($userId);
×
119

120
        if ($user === null) {
×
121
            return $this->response->setStatusCode(401)->setJSON([
×
122
                'message' => lang('Auth.invalidUser'),
×
123
            ]);
×
124
        }
125

126
        return $this->response->setJSON($this->buildTokenResponse($user));
×
127
    }
128

129
    /**
130
     * Revoke the refresh token (stateless logout).
131
     *
132
     * POST body: user_id, refresh_token
133
     * Response JSON: message
134
     */
135
    public function logout(): ResponseInterface
×
136
    {
137
        $userId       = (int) $this->request->getPost('user_id');
×
138
        $refreshToken = (string) $this->request->getPost('refresh_token');
×
139

140
        if ($userId > 0 && $refreshToken !== '') {
×
141
            /** @var UserIdentityModel $identityModel */
142
            $identityModel = model(UserIdentityModel::class);
×
143
            $identity      = $identityModel->getJwtRefreshToken($userId, $refreshToken);
×
144

145
            if ($identity !== null) {
×
146
                $identityModel->revokeIdentityById((int) $identity->id);
×
147

148
                (new AuditLogger())->record(AuditLogger::EVENT_REFRESH_TOKEN_REVOKED, $userId, [
×
149
                    'identity_id' => (int) $identity->id,
×
150
                    'reason'      => 'logout',
×
151
                ]);
×
152
            }
153
        }
154

155
        return $this->response->setJSON(['message' => lang('Auth.successLogout')]);
×
156
    }
157

158
    /**
159
     * Builds the token pair response for a given user.
160
     *
161
     * Generates a JWT access token (via the configured adapter) and a new
162
     * opaque refresh token stored in the identities table.
163
     *
164
     * @param User $user
165
     *
166
     * @return array<string, mixed>
167
     */
168
    private function buildTokenResponse(object $user): array
×
169
    {
170
        // Generate access token via the configured JWT adapter
171
        $adapterClass = setting('Auth.jwtAdapter');
×
172
        /** @var JWTAdapterInterface $adapter */
173
        $adapter     = new $adapterClass();
×
174
        $accessToken = $adapter->encode($user->id);
×
175

176
        // Generate and persist a new refresh token
177
        $rawRefresh = bin2hex(random_bytes(32));
×
178
        $expiresAt  = Time::now()
×
179
            ->addSeconds((int) setting('AuthSecurity.jwtRefreshLifetime'))
×
180
            ->format('Y-m-d H:i:s');
×
181

182
        /** @var UserIdentityModel $identityModel */
183
        $identityModel = model(UserIdentityModel::class);
×
184
        $identityModel->createJwtRefreshToken((int) $user->id, $rawRefresh, $expiresAt);
×
185

186
        return [
×
187
            'access_token'  => $accessToken,
×
188
            'refresh_token' => $rawRefresh,
×
189
            'user_id'       => $user->id,
×
190
            'token_type'    => 'Bearer',
×
191
        ];
×
192
    }
193
}
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