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

daycry / auth / 25518434194

07 May 2026 07:49PM UTC coverage: 58.608% (-6.4%) from 64.989%
25518434194

push

github

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

Implement security enhancements and new account features

277 of 1030 new or added lines in 55 files covered. (26.89%)

11 existing lines in 6 files now uncovered.

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\Entities\User;
19
use Daycry\Auth\Interfaces\JWTAdapterInterface;
20
use Daycry\Auth\Models\UserIdentityModel;
21
use Daycry\Auth\Models\UserModel;
22
use Daycry\Auth\Services\AuditLogger;
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

UNCOV
70
        $authenticator = $this->getSessionAuthenticator();
×
71
        $result        = $authenticator->check($credentials);
×
72

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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