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

daycry / auth / 22527357078

28 Feb 2026 07:22PM UTC coverage: 63.267% (+0.7%) from 62.568%
22527357078

push

github

daycry
Remove PHP 8.1 from PHPUnit CI matrix

Update .github/workflows/phpunit.yml to drop PHP 8.1 from the test matrix. CI will now run PHPUnit only on PHP 8.2 and 8.3, reducing the matrix to current supported versions.

3064 of 4843 relevant lines covered (63.27%)

41.52 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\Validation\ValidationRules;
24

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

149
        return $this->response->setJSON(['message' => lang('Auth.successLogout')]);
×
150
    }
151

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

170
        // Generate and persist a new refresh token
171
        $rawRefresh = bin2hex(random_bytes(32));
×
172
        $expiresAt  = Time::now()
×
173
            ->addSeconds((int) setting('Auth.jwtRefreshLifetime'))
×
174
            ->format('Y-m-d H:i:s');
×
175

176
        /** @var UserIdentityModel $identityModel */
177
        $identityModel = model(UserIdentityModel::class);
×
178
        $identityModel->createJwtRefreshToken((int) $user->id, $rawRefresh, $expiresAt);
×
179

180
        return [
×
181
            'access_token'  => $accessToken,
×
182
            'refresh_token' => $rawRefresh,
×
183
            'user_id'       => $user->id,
×
184
            'token_type'    => 'Bearer',
×
185
        ];
×
186
    }
187
}
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