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

avoutic / web-framework / 19992775795

06 Dec 2025 06:45PM UTC coverage: 72.97% (-0.009%) from 72.979%
19992775795

push

github

avoutic
Use modern find() variants

27 of 29 new or added lines in 6 files covered. (93.1%)

31 existing lines in 4 files now uncovered.

1995 of 2734 relevant lines covered (72.97%)

2.76 hits per line

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

86.21
/src/Security/ChangeEmailService.php
1
<?php
2

3
/*
4
 * This file is part of WebFramework.
5
 *
6
 * (c) Avoutic <avoutic@gmail.com>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11

12
namespace WebFramework\Security;
13

14
use Psr\Log\LoggerInterface;
15
use Slim\Http\ServerRequest as Request;
16
use WebFramework\Entity\User;
17
use WebFramework\Event\EventService;
18
use WebFramework\Event\UserEmailChanged;
19
use WebFramework\Exception\CodeVerificationException;
20
use WebFramework\Exception\DuplicateEmailException;
21
use WebFramework\Exception\WrongAccountException;
22
use WebFramework\Mail\UserMailer;
23
use WebFramework\Repository\UserRepository;
24
use WebFramework\Repository\VerificationCodeRepository;
25

26
/**
27
 * Class ChangeEmailService.
28
 *
29
 * Handles the process of changing a user's email address.
30
 */
31
class ChangeEmailService
32
{
33
    /**
34
     * ChangeEmailService constructor.
35
     *
36
     * @param AuthenticationService      $authenticationService      The authentication service
37
     * @param EventService               $eventService               The event service
38
     * @param LoggerInterface            $logger                     The logger service
39
     * @param SecurityIteratorService    $securityIteratorService    The security iterator service
40
     * @param UserCodeService            $userCodeService            The user code service
41
     * @param UserMailer                 $userMailer                 The user mailer service
42
     * @param UserRepository             $userRepository             The user repository
43
     * @param VerificationCodeRepository $verificationCodeRepository The verification code repository
44
     * @param int                        $codeExpiryMinutes          The number of minutes until verification codes expire
45
     * @param string                     $uniqueIdentifier           The unique identifier type ('email' or 'username')
46
     */
47
    public function __construct(
×
48
        private AuthenticationService $authenticationService,
49
        private EventService $eventService,
50
        private LoggerInterface $logger,
51
        private SecurityIteratorService $securityIteratorService,
52
        private UserCodeService $userCodeService,
53
        private UserMailer $userMailer,
54
        private UserRepository $userRepository,
55
        private VerificationCodeRepository $verificationCodeRepository,
56
        private int $codeExpiryMinutes,
57
        private string $uniqueIdentifier,
58
    ) {}
×
59

60
    /**
61
     * Change the email address for a user.
62
     *
63
     * @param Request $request The request that triggered the event
64
     * @param User    $user    The user whose email is being changed
65
     * @param string  $email   The new email address
66
     *
67
     * @throws DuplicateEmailException If the email already exists and is the unique identifier
68
     */
69
    public function changeEmail(Request $request, User $user, string $email): void
4✔
70
    {
71
        if ($this->uniqueIdentifier == 'email')
4✔
72
        {
73
            $exists = $this->userRepository
2✔
74
                ->query(['email' => $email])
2✔
75
                ->exists()
2✔
76
            ;
2✔
77

78
            if ($exists)
2✔
79
            {
80
                $this->logger->debug('E-mail address already exists', ['email' => $email]);
1✔
81

82
                throw new DuplicateEmailException('E-mail address already exists');
1✔
83
            }
84
        }
85

86
        $this->logger->info('Changing email address', ['user_id' => $user->getId(), 'email' => $email]);
3✔
87

88
        // Update account
89
        //
90
        $user->setEmail($email);
3✔
91

92
        if ($this->uniqueIdentifier == 'email')
3✔
93
        {
94
            $this->logger->info('Setting username to email', ['user_id' => $user->getId(), 'email' => $email]);
1✔
95

96
            $user->setUsername($email);
1✔
97
        }
98

99
        $this->userRepository->save($user);
3✔
100

101
        $this->eventService->dispatch(new UserEmailChanged($request, $user));
3✔
102
    }
103

104
    /**
105
     * Send a verification email for changing the email address.
106
     *
107
     * @param User   $user  The user requesting the email change
108
     * @param string $email The new email address
109
     *
110
     * @return string The GUID for passing to the verify action
111
     *
112
     * @throws DuplicateEmailException If the email already exists and is the unique identifier
113
     */
114
    public function sendChangeEmailVerify(User $user, string $email): string
2✔
115
    {
116
        if ($this->uniqueIdentifier == 'email')
2✔
117
        {
118
            $exists = $this->userRepository
2✔
119
                ->query(['email' => $email])
2✔
120
                ->exists()
2✔
121
            ;
2✔
122

123
            if ($exists)
2✔
124
            {
125
                $this->logger->debug('E-mail address already exists', ['email' => $email]);
1✔
126

127
                throw new DuplicateEmailException('E-mail address already exists');
1✔
128
            }
129
        }
130

131
        $securityIterator = $this->securityIteratorService->incrementFor($user);
1✔
132

133
        ['guid' => $guid, 'code' => $code] = $this->userCodeService->generateVerificationCodeEntry(
1✔
134
            $user,
1✔
135
            'change_email',
1✔
136
            [
1✔
137
                'email' => $email,
1✔
138
                'iterator' => $securityIterator,
1✔
139
            ]
1✔
140
        );
1✔
141

142
        $this->logger->debug('Sending change email verification link', ['user_id' => $user->getId(), 'email' => $email]);
1✔
143

144
        $this->userMailer->changeEmailVerificationCode(
1✔
145
            $email,
1✔
146
            [
1✔
147
                'user' => $user->toArray(),
1✔
148
                'code' => $code,
1✔
149
                'validity' => $this->codeExpiryMinutes,
1✔
150
            ]
1✔
151
        );
1✔
152

153
        return $guid;
1✔
154
    }
155

156
    /**
157
     * Handle the data for an email change request.
158
     *
159
     * @param Request $request The request that triggered the event
160
     * @param User    $user    The user verifying the email change
161
     * @param string  $guid    The GUID of the verification code (already verified)
162
     *
163
     * @throws CodeVerificationException If the data is invalid
164
     * @throws WrongAccountException     If the code doesn't match the current user
165
     */
166
    public function handleData(Request $request, User $user, string $guid): void
6✔
167
    {
168
        $this->logger->debug('Handling change email data', ['guid' => $guid]);
6✔
169

170
        $verificationCode = $this->verificationCodeRepository->getByGuid($guid);
6✔
171

172
        if ($verificationCode === null)
6✔
173
        {
174
            throw new CodeVerificationException();
1✔
175
        }
176

177
        // Verify the code was actually verified (marked as used) and hasn't expired
178
        if (!$verificationCode->isCorrect())
5✔
179
        {
180
            $this->logger->debug('Verification code not yet correct', ['guid' => $guid]);
1✔
181

182
            throw new CodeVerificationException();
1✔
183
        }
184

185
        if ($verificationCode->isExpired())
4✔
186
        {
UNCOV
187
            $this->logger->debug('Verification code expired', ['guid' => $guid]);
×
188

189
            throw new CodeVerificationException();
×
190
        }
191

192
        // Prevent replay attacks - check if code has already been invalidated or processed
193
        if ($verificationCode->isInvalidated() || $verificationCode->isProcessed())
4✔
194
        {
UNCOV
195
            $this->logger->warning('Verification code already invalidated or processed (replay attempt)', ['guid' => $guid]);
×
196

197
            throw new CodeVerificationException();
×
198
        }
199

200
        if ($verificationCode->getAction() !== 'change_email')
4✔
201
        {
UNCOV
202
            $this->logger->error('Invalid action for verification code', [
×
UNCOV
203
                'guid' => $guid,
×
UNCOV
204
                'action' => $verificationCode->getAction(),
×
UNCOV
205
            ]);
×
206

207
            throw new CodeVerificationException();
×
208
        }
209

210
        $codeUser = $this->userRepository->find($verificationCode->getUserId());
4✔
211
        if ($codeUser === null)
4✔
212
        {
UNCOV
213
            throw new CodeVerificationException();
×
214
        }
215

216
        // Only allow for current user
217
        //
218
        if ($codeUser->getId() !== $user->getId())
4✔
219
        {
220
            $this->logger->debug('Received change email verify for wrong account', ['user_id' => $user->getId(), 'code_user_id' => $codeUser->getId()]);
1✔
221

222
            $this->authenticationService->deauthenticate();
1✔
223

224
            throw new WrongAccountException();
1✔
225
        }
226

227
        $flowData = $verificationCode->getFlowData();
3✔
228
        $email = $flowData['email'] ?? '';
3✔
229

230
        // Already changed
231
        //
232
        if ($user->getEmail() === $email)
3✔
233
        {
234
            $this->logger->debug('Already changed email address', ['user_id' => $user->getId(), 'email' => $email]);
1✔
235

236
            return;
1✔
237
        }
238

239
        // Change email
240
        //
241
        $securityIterator = $this->securityIteratorService->getFor($user);
2✔
242

243
        if (!isset($flowData['iterator']) || $securityIterator != $flowData['iterator'])
2✔
244
        {
245
            $this->logger->debug('Change email verification has old iterator', ['user_id' => $user->getId(), 'email' => $email]);
1✔
246

247
            throw new CodeVerificationException();
1✔
248
        }
249

250
        // Mark as processed to prevent replay attacks
251
        $verificationCode->markAsProcessed();
1✔
252
        $this->verificationCodeRepository->save($verificationCode);
1✔
253

254
        $this->changeEmail($request, $user, $email);
1✔
255

256
        // Invalidate old sessions
257
        //
258
        $this->authenticationService->invalidateSessions($user->getId());
1✔
259
        $this->authenticationService->authenticate($user);
1✔
260
    }
261
}
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