Passed
Push — master ( 2ef570...6afb0c )
by Tim
09:00 queued 01:16
created

PasswordReset::enterEmail()   B

Complexity

Conditions 5
Paths 5

Size

Total Lines 66
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 43
nc 5
nop 1
dl 0
loc 66
rs 8.9208
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace SimpleSAML\Module\ldapPasswordReset\Controller;
4
5
use Exception;
6
use SimpleSAML\Assert\Assert;
7
use SimpleSAML\{Auth, Configuration, Error, Logger, Module, Session};
8
use SimpleSAML\Module\ldapPasswordReset\MagicLink;
9
use SimpleSAML\Module\ldapPasswordReset\TokenStorage;
10
use SimpleSAML\Module\ldapPasswordReset\UserRepository;
11
use SimpleSAML\XHTML\Template;
12
use Symfony\Component\HttpFoundation\{RedirectResponse, Request};
13
14
use function date;
15
use function sprintf;
16
use function time;
17
18
/**
19
 * Controller class for the ldapPasswordReset module.
20
 *
21
 * This class serves the password reset code and error views available in the module.
22
 *
23
 * @package simplesamlphp/simplesamlphp-module-ldapPasswordReset
24
 */
25
class PasswordReset
26
{
27
    /** @var \SimpleSAML\Configuration */
28
    protected Configuration $config;
29
30
    /** @var \SimpleSAML\Logger */
31
    protected Logger $logger;
32
33
    /** @var \SimpleSAML\Configuration */
34
    protected Configuration $moduleConfig;
35
36
    /** @var \SimpleSAML\Session */
37
    protected Session $session;
38
39
    /** @var \SimpleSAML\Module\ldapPasswordReset\UserRepository */
40
    protected UserRepository $userRepository;
41
42
    /**
43
     * @var \SimpleSAML\Auth\State|string
44
     * @psalm-var \SimpleSAML\Auth\State|class-string
45
     */
46
    protected $authState = Auth\State::class;
47
48
49
    /**
50
     * Password reset Controller constructor.
51
     *
52
     * @param \SimpleSAML\Configuration $config The configuration to use.
53
     * @param \SimpleSAML\Session $session The current user session.
54
     */
55
    public function __construct(Configuration $config, Session $session)
56
    {
57
        $this->config = $config;
58
        $this->logger = new Logger();
59
        $this->moduleConfig = Configuration::getConfig('module_ldapPasswordReset.php');
60
        $this->session = $session;
61
        $this->userRepository = new UserRepository();
62
    }
63
64
65
    /**
66
     * Inject the \SimpleSAML\Logger dependency.
67
     *
68
     * @param \SimpleSAML\Logger $logger
69
     */
70
    public function setLogger(Logger $logger): void
71
    {
72
        $this->logger = $logger;
73
    }
74
75
76
    /**
77
     * Inject the \SimpleSAML\Auth\State dependency.
78
     *
79
     * @param \SimpleSAML\Auth\State $authState
80
     */
81
    public function setAuthState(Auth\State $authState): void
82
    {
83
        $this->authState = $authState;
84
    }
85
86
87
    /**
88
     * Display the page where the EMail address should be entered.
89
     *
90
     * @return \SimpleSAML\XHTML\Template
91
     */
92
    public function enterEmail(Request $request): Template
93
    {
94
        $t = new Template($this->config, 'ldapPasswordReset:enterEmail.twig');
95
        $t->data = [
96
            'mailSent' => false,
97
        ];
98
99
        $state = [];
100
        if ($request->request->has('submit_button')) {
101
            /** @psalm-var string|null $id */
102
            $id = $request->query->get('AuthState', null);
103
            if ($id === null) {
104
                throw new Error\BadRequest('Missing AuthState parameter.');
105
            }
106
107
            $t->data['mailSent'] = true;
108
109
            /** @psalm-var string $email */
110
            $email = $request->request->get('email');
111
            Assert::stringNotEmpty($email);
112
113
            /** @var array $state */
114
            $state = $this->authState::loadState($id, 'ldapPasswordReset:request', false);
115
116
            $user = $this->userRepository->findUserByEmail($email);
117
            if ($user !== null) {
118
                $this->logger::info(sprintf('ldapPasswordReset: a password reset was requested for user %s', $email));
119
120
                $tokenStorage = new TokenStorage($this->config);
121
                $token = $tokenStorage->generateToken();
122
                $session = $this->session->getTrackID();
123
                $validUntil = time() + ($this->moduleConfig->getOptionalInteger('magicLinkExpiration', 15) * 60);
124
125
                $tokenStorage->storeToken(
126
                    $token,
127
                    $email,
128
                    $session,
129
                    $validUntil,
130
                    $state['ldapPasswordReset:referer'] ?? null
131
                );
132
                $this->logger::debug(sprintf('ldapPasswordReset: token %s was stored for %s', $token, $email));
133
134
                $mailer = new MagicLink($this->config);
135
                $mailer->sendMagicLink($email, $token, $validUntil);
136
                $this->logger::info(sprintf(
137
                    'ldapPasswordReset: token %s was e-mailed to user %s (valid until %s)',
138
                    $token,
139
                    $email,
140
                    date(DATE_RFC2822, $validUntil))
141
                );
142
            } else {
143
                $this->logger::warning(sprintf(
144
                    'ldapPasswordReset: a password reset was requested for non-existing user %s',
145
                    $email
146
                ));
147
            }
148
        } else {
149
            $state = [];
150
151
            if ($request->server->has('HTTP_REFERER')) {
152
                $state['ldapPasswordReset:referer'] = $request->server->get('HTTP_REFERER');
153
            }
154
        }
155
156
        $t->data['AuthState'] = $this->authState::saveState($state, 'ldapPasswordReset:request');
157
        return $t;
158
    }
159
160
161
    /**
162
     * Process a received magic link.
163
     *
164
     * @return \Symfony\Component\HttpFoundation\RedirectResponse
165
     */
166
    public function validateMagicLink(Request $request): RedirectResponse
167
    {
168
        if (!$request->query->has('t')) {
169
            throw new Error\BadRequest('Missing token.');
170
        }
171
172
        /** @psalm-var string $t */
173
        $t = $request->query->get('t');
174
        Assert::uuid($t, 'Invalid token provided.', Error\BadRequest::class);
175
176
        $tokenStorage = new TokenStorage($this->config);
177
        $token = $tokenStorage->retrieveToken($t);
178
179
        Assert::nullOrIsArray($token);
180
181
        if ($token !== null) {
182
            Assert::keyExists($token, 'mail');
183
            Assert::keyExists($token, 'session');
184
            Assert::keyExists($token, 'referer');
185
186
            if (
187
                $this->moduleConfig->getOptionalBoolean('lockBrowserSession', true)
188
                && $token['session'] !== $this->session->getTrackID()
189
            ) {
190
                $this->logger::warning(sprintf(
191
                    "Token '%s' was used in a different browser session then where it was requested from.",
192
                    $t,
193
                ));
194
            } else {
195
                $this->logger::info(sprintf(
196
                    "ldapPasswordReset: pre-conditions for token '%s' were met. User '%s' may change it's password.",
197
                    $t,
198
                    $token['mail']
199
                ));
200
201
                // All pre-conditions met - Allow user to change password
202
                $state = [
203
                    'ldapPasswordReset:magicLinkValidated' => true,
204
                    'ldapPasswordReset:subject' => $token['mail'],
205
                    'ldapPasswordReset:session' => $token['session'],
206
                    'ldapPasswordReset:token' => $t,
207
                    'ldapPasswordReset:referer' => $token['referer'],
208
                ];
209
210
                // Invalidate token - It may be used only once to reset a password
211
//                $tokenStorage->deleteToken($t);
212
213
                $id = $this->authState::saveState($state, 'ldapPasswordReset:request');
214
                return new RedirectResponse(
215
                    Module::getModuleURL('ldapPasswordReset/resetPassword', ['AuthState' => $id])
216
                );
217
            }
218
        } else {
219
            $this->logger::warning(sprintf("ldapPasswordReset: Could not find token '%s' in token storage.", $t));
220
        }
221
222
        $this->logger::debug(sprintf("ldapPasswordReset: an invalid magic link was used: %s", $t));
223
        return new RedirectResponse(Module::getModuleURL('ldapPasswordReset/invalidMagicLink'));
224
    }
225
226
227
    /**
228
     * Display an error message when an invalid magic link was used.
229
     *
230
     * @return \SimpleSAML\XHTML\Template
231
     */
232
    public function invalidMagicLink(Request $request): Template
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

232
    public function invalidMagicLink(/** @scrutinizer ignore-unused */ Request $request): Template

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
233
    {
234
        return new Template($this->config, 'ldapPasswordReset:invalidMagicLink.twig');
235
    }
236
237
238
    /**
239
     * Display the page where the user can set a new password.
240
     *
241
     * @return \SimpleSAML\XHTML\Template
242
     */
243
    public function resetPassword(Request $request): Template
244
    {
245
        /** @psalm-var string|null $id */
246
        $id = $request->query->get('AuthState', null);
247
        if ($id === null) {
248
            throw new Error\BadRequest('Missing AuthState parameter.');
249
        }
250
251
        /** @var array $state */
252
        $state = $this->authState::loadState($id, 'ldapPasswordReset:request', false);
253
254
        $t = new Template($this->config, 'ldapPasswordReset:resetPassword.twig');
255
        $t->data = [
256
            'AuthState' => $id,
257
            'passwordMismatch' => false,
258
            'emailAddress' => $state['ldapPasswordReset:subject'],
259
        ];
260
261
        // Check if the submit-button was hit, or whether this is a first visit
262
        if ($request->request->has('submit_button')) {
263
            $this->logger::debug(sprintf(
264
                'ldapPasswordReset: a new password was entered for user %s',
265
                $state['ldapPasswordReset:subject'])
266
            );
267
268
            // See if the submitted passwords match
269
            /** @psalm-var string $newPassword */
270
            $newPassword = $request->request->get('new-password');
271
            /** @psalm-var string $retypePassword */
272
            $retypePassword = $request->request->get('password');
273
274
            if (strcmp($newPassword, $retypePassword) === 0) {
275
                $this->logger::debug(sprintf(
276
                    'ldapPasswordReset: new matching passwords were entered for user %s',
277
                    $state['ldapPasswordReset:subject']
278
                ));
279
280
                $user = $this->userRepository->findUserByEmail($state['ldapPasswordReset:subject']);
281
                Assert::notNull($user); // Must exist
282
283
                $result = $this->userRepository->updatePassword($user, $newPassword);
0 ignored issues
show
Bug introduced by
It seems like $user can also be of type null; however, parameter $user of SimpleSAML\Module\ldapPa...itory::updatePassword() does only seem to accept Symfony\Component\Ldap\Entry, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

283
                $result = $this->userRepository->updatePassword(/** @scrutinizer ignore-type */ $user, $newPassword);
Loading history...
284
                if ($result === true) {
285
                    $this->logger::info(sprintf(
286
                        'Password was reset for user: %s',
287
                        $state['ldapPasswordReset:subject']
288
                    ));
289
290
                    $t = new Template($this->config, 'ldapPasswordReset:passwordChanged.twig');
291
                    if (
292
                        isset($state['ldapPasswordReset:referer'])
293
                        && ($state['session'] === $this->session->getTrackID())
294
                    ) {
295
                        // If this isn't the same browser, it makes no sense to get back to the
296
                        // previous authentication-flow. It will fail relentlessly
297
                        $t->data['referer'] = $state['ldapPasswordReset:referer'];
298
                    }
299
                    $t->data['passwordChanged'] = true;
300
301
                    // Invalidate token - It may be used only once to reset a password
302
                    $tokenStorage = new TokenStorage($this->config);
303
                    $tokenStorage->deleteToken($state['ldapPasswordReset:token']);
304
305
                    return $t;
306
                } else {
307
                    $this->logger::warning(sprintf(
308
                        'Password reset has failed for user: %s',
309
                        $state['ldapPasswordReset:subject']
310
                    ));
311
312
                    $t->data['passwordChanged'] = false;
313
                }
314
            } else {
315
                $this->logger::debug(sprintf(
316
                    'ldapPasswordReset: mismatching passwords were entered for user %s',
317
                    $state['ldapPasswordReset:subject']
318
                ));
319
                $t->data['passwordMismatch'] = true;
320
            }
321
        }
322
323
        return $t;
324
    }
325
}
326