Completed
Push — develop ( f77fd9...d910ff )
by Abdelrahman
10:08
created

PasswordResetBroker::reset()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 20
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 2
dl 0
loc 20
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Auth\Services;
6
7
use Closure;
8
use Illuminate\Support\Arr;
9
use Illuminate\Support\Str;
10
use UnexpectedValueException;
11
use Illuminate\Contracts\Auth\UserProvider;
12
use Rinvex\Auth\Contracts\CanResetPasswordContract;
13
use Rinvex\Auth\Contracts\PasswordResetBrokerContract;
14
15
class PasswordResetBroker implements PasswordResetBrokerContract
16
{
17
    /**
18
     * The application key.
19
     *
20
     * @var string
21
     */
22
    protected $key;
23
24
    /**
25
     * The user provider implementation.
26
     *
27
     * @var \Illuminate\Contracts\Auth\UserProvider
28
     */
29
    protected $users;
30
31
    /**
32
     * The number of minutes that the reset token should be considered valid.
33
     *
34
     * @var int
35
     */
36
    protected $expiration;
37
38
    /**
39
     * The custom password validator callback.
40
     *
41
     * @var \Closure
42
     */
43
    protected $passwordValidator;
44
45
    /**
46
     * Create a new verification broker instance.
47
     *
48
     * @param \Illuminate\Contracts\Auth\UserProvider $users
49
     * @param string                                  $key
50
     * @param int                                     $expiration
51
     */
52
    public function __construct(UserProvider $users, $key, $expiration)
53
    {
54
        $this->key = $key;
55
        $this->users = $users;
56
        $this->expiration = $expiration;
57
    }
58
59
    /**
60
     * Send a password reset link to a user.
61
     *
62
     * @param array $credentials
63
     *
64
     * @return string
65
     */
66
    public function sendResetLink(array $credentials): string
67
    {
68
        // First we will check to see if we found a user at the given credentials and
69
        // if we did not we will redirect back to this current URI with a piece of
70
        // "flash" data in the session to indicate to the developers the errors.
71
        $user = $this->getUser($credentials);
72
73
        if (is_null($user)) {
74
            return static::INVALID_USER;
75
        }
76
77
        $expiration = now()->addMinutes($this->expiration)->timestamp;
78
79
        // Once we have the reset token, we are ready to send the message out to this
80
        // user with a link to reset their password. We will then redirect back to
81
        // the current URI having nothing set in the session to indicate errors.
82
        $user->sendPasswordResetNotification($this->createToken($user, $expiration), $expiration);
83
84
        return static::RESET_LINK_SENT;
85
    }
86
87
    /**
88
     * Reset the password for the given token.
89
     *
90
     * @param array    $credentials
91
     * @param \Closure $callback
92
     *
93
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
94
     */
95
    public function reset(array $credentials, Closure $callback)
96
    {
97
        // If the responses from the validate method is not a user instance, we will
98
        // assume that it is a redirect and simply return it from this method and
99
        // the user is properly redirected having an error message on the post.
100
        $user = $this->validateReset($credentials);
101
102
        if (! $user instanceof CanResetPasswordContract) {
103
            return $user;
104
        }
105
106
        $password = $credentials['password'];
107
108
        // Once the reset has been validated, we'll call the given callback with the
109
        // new password. This gives the user an opportunity to store the password
110
        // in their persistent storage.
111
        $callback($user, $password);
112
113
        return static::PASSWORD_RESET;
114
    }
115
116
    /**
117
     * Set a custom password validator.
118
     *
119
     * @param \Closure $callback
120
     *
121
     * @return void
122
     */
123
    public function validator(Closure $callback): void
124
    {
125
        $this->passwordValidator = $callback;
126
    }
127
128
    /**
129
     * Determine if the passwords match for the request.
130
     *
131
     * @param array $credentials
132
     *
133
     * @return bool
134
     */
135
    public function validateNewPassword(array $credentials): bool
136
    {
137
        if (isset($this->passwordValidator)) {
138
            list($password, $confirm) = [
139
                $credentials['password'],
140
                $credentials['password_confirmation'],
141
            ];
142
143
            return call_user_func(
144
                $this->passwordValidator, $credentials
145
            ) && $password === $confirm;
146
        }
147
148
        return $this->validatePasswordWithDefaults($credentials);
149
    }
150
151
    /**
152
     * Determine if the passwords are valid for the request.
153
     *
154
     * @param array $credentials
155
     *
156
     * @return bool
157
     */
158
    protected function validatePasswordWithDefaults(array $credentials): bool
159
    {
160
        list($password, $confirm) = [
161
            $credentials['password'],
162
            $credentials['password_confirmation'],
163
        ];
164
165
        return $password === $confirm && mb_strlen($password) >= 6;
166
    }
167
168
    /**
169
     * Get the user for the given credentials.
170
     *
171
     * @param array $credentials
172
     *
173
     * @throws \UnexpectedValueException
174
     *
175
     * @return \Rinvex\Auth\Contracts\CanResetPasswordContract
176
     */
177
    public function getUser(array $credentials): CanResetPasswordContract
178
    {
179
        $user = $this->users->retrieveByCredentials(Arr::only($credentials, ['email']));
180
181
        if ($user && ! $user instanceof CanResetPasswordContract) {
182
            throw new UnexpectedValueException('User must implement CanResetPassword interface.');
183
        }
184
185
        return $user;
186
    }
187
188
    /**
189
     * Create a new password reset token for the given user.
190
     *
191
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
192
     * @param int                                             $expiration
193
     *
194
     * @return string
195
     */
196
    public function createToken(CanResetPasswordContract $user, $expiration): string
197
    {
198
        $payload = $this->buildPayload($user, $user->getEmailForPasswordReset(), $expiration);
199
200
        return hash_hmac('sha256', $payload, $this->getKey());
201
    }
202
203
    /**
204
     * Validate the given password reset token.
205
     *
206
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
207
     * @param array                                           $credentials
208
     *
209
     * @return bool
210
     */
211
    public function validateToken(CanResetPasswordContract $user, array $credentials): bool
212
    {
213
        $payload = $this->buildPayload($user, $credentials['email'], $credentials['expiration']);
214
215
        return hash_equals($credentials['token'], hash_hmac('sha256', $payload, $this->getKey()));
216
    }
217
218
    /**
219
     * Validate the given expiration timestamp.
220
     *
221
     * @param int $expiration
222
     *
223
     * @return bool
224
     */
225
    public function validateTimestamp($expiration): bool
226
    {
227
        return now()->createFromTimestamp($expiration)->isFuture();
228
    }
229
230
    /**
231
     * Return the application key.
232
     *
233
     * @return string
234
     */
235
    public function getKey(): string
236
    {
237
        if (Str::startsWith($this->key, 'base64:')) {
238
            return base64_decode(mb_substr($this->key, 7));
239
        }
240
241
        return $this->key;
242
    }
243
244
    /**
245
     * Returns the payload string containing.
246
     *
247
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
248
     * @param string                                          $email
249
     * @param int                                             $expiration
250
     *
251
     * @return string
252
     */
253
    protected function buildPayload(CanResetPasswordContract $user, $email, $expiration): string
254
    {
255
        return implode(';', [
256
            $email,
257
            $expiration,
258
            $user->getKey(),
0 ignored issues
show
Bug introduced by
The method getKey() does not seem to exist on object<Rinvex\Auth\Contr...nResetPasswordContract>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
259
            $user->password,
0 ignored issues
show
Bug introduced by
Accessing password on the interface Rinvex\Auth\Contracts\CanResetPasswordContract suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
260
        ]);
261
    }
262
263
    /**
264
     * Validate a password reset for the given credentials.
265
     *
266
     * @param array $credentials
267
     *
268
     * @return \Illuminate\Contracts\Auth\CanResetPassword|string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|CanResetPasswordContract?

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

Loading history...
269
     */
270
    protected function validateReset(array $credentials)
271
    {
272
        if (is_null($user = $this->getUser($credentials))) {
273
            return static::INVALID_USER;
274
        }
275
276
        if (! $this->validateNewPassword($credentials)) {
277
            return static::INVALID_PASSWORD;
278
        }
279
280
        if (! $this->validateToken($user, $credentials)) {
281
            return static::INVALID_TOKEN;
282
        }
283
284
        if (! $this->validateTimestamp($credentials['expiration'])) {
285
            return static::EXPIRED_TOKEN;
286
        }
287
288
        return $user;
289
    }
290
}
291