Completed
Push — master ( 425779...b6e961 )
by Abdelrahman
08:30
created

PasswordResetBroker::getKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
84
85
        return static::RESET_LINK_SENT;
86
    }
87
88
    /**
89
     * Reset the password for the given token.
90
     *
91
     * @param array    $credentials
92
     * @param \Closure $callback
93
     *
94
     * @return mixed
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use string|\Illuminate\Contracts\Auth\Authenticatable.

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...
95
     */
96
    public function reset(array $credentials, Closure $callback)
97
    {
98
        // If the responses from the validate method is not a user instance, we will
99
        // assume that it is a redirect and simply return it from this method and
100
        // the user is properly redirected having an error message on the post.
101
        $user = $this->validateReset($credentials);
102
103
        if (! $user instanceof CanResetPasswordContract) {
104
            return $user;
105
        }
106
107
        $password = $credentials['password'];
108
109
        // Once the reset has been validated, we'll call the given callback with the
110
        // new password. This gives the user an opportunity to store the password
111
        // in their persistent storage.
112
        $callback($user, $password);
113
114
        return static::PASSWORD_RESET;
115
    }
116
117
    /**
118
     * Set a custom password validator.
119
     *
120
     * @param \Closure $callback
121
     *
122
     * @return void
123
     */
124
    public function validator(Closure $callback)
125
    {
126
        $this->passwordValidator = $callback;
127
    }
128
129
    /**
130
     * Determine if the passwords match for the request.
131
     *
132
     * @param array $credentials
133
     *
134
     * @return bool
135
     */
136
    public function validateNewPassword(array $credentials)
137
    {
138
        if (isset($this->passwordValidator)) {
139
            list($password, $confirm) = [
140
                $credentials['password'],
141
                $credentials['password_confirmation'],
142
            ];
143
144
            return call_user_func(
145
                $this->passwordValidator, $credentials
146
            ) && $password === $confirm;
147
        }
148
149
        return $this->validatePasswordWithDefaults($credentials);
150
    }
151
152
    /**
153
     * Determine if the passwords are valid for the request.
154
     *
155
     * @param array $credentials
156
     *
157
     * @return bool
158
     */
159
    protected function validatePasswordWithDefaults(array $credentials)
160
    {
161
        list($password, $confirm) = [
162
            $credentials['password'],
163
            $credentials['password_confirmation'],
164
        ];
165
166
        return $password === $confirm && mb_strlen($password) >= 6;
167
    }
168
169
    /**
170
     * Get the user for the given credentials.
171
     *
172
     * @param array $credentials
173
     *
174
     * @throws \UnexpectedValueException
175
     *
176
     * @return \Rinvex\Fort\Contracts\CanResetPasswordContract
0 ignored issues
show
Documentation introduced by
Should the return type not be \Illuminate\Contracts\Auth\Authenticatable|null?

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...
177
     */
178
    public function getUser(array $credentials)
179
    {
180
        $user = $this->users->retrieveByCredentials(Arr::only($credentials, ['email']));
181
182
        if ($user && ! $user instanceof CanResetPasswordContract) {
183
            throw new UnexpectedValueException('User must implement CanResetPassword interface.');
184
        }
185
186
        return $user;
187
    }
188
189
    /**
190
     * Create a new password reset token for the given user.
191
     *
192
     * @param \Rinvex\Fort\Contracts\CanResetPasswordContract $user
193
     * @param int                                             $expiration
194
     *
195
     * @return string
196
     */
197
    public function createToken(CanResetPasswordContract $user, $expiration)
198
    {
199
        $payload = $this->buildPayload($user, $user->getEmailForPasswordReset(), $expiration);
200
201
        return hash_hmac('sha256', $payload, $this->getKey());
202
    }
203
204
    /**
205
     * Validate the given password reset token.
206
     *
207
     * @param \Rinvex\Fort\Contracts\CanResetPasswordContract $user
208
     * @param array                                           $credentials
209
     *
210
     * @return bool
211
     */
212
    public function validateToken(CanResetPasswordContract $user, array $credentials)
213
    {
214
        $payload = $this->buildPayload($user, $credentials['email'], $credentials['expiration']);
215
216
        return hash_equals($credentials['token'], hash_hmac('sha256', $payload, $this->getKey()));
217
    }
218
219
    /**
220
     * Validate the given expiration timestamp.
221
     *
222
     * @param int $expiration
223
     *
224
     * @return bool
225
     */
226
    public function validateTimestamp($expiration)
227
    {
228
        return Carbon::createFromTimestamp($expiration)->isFuture();
229
    }
230
231
    /**
232
     * Return the application key.
233
     *
234
     * @return string
235
     */
236
    public function getKey()
237
    {
238
        if (Str::startsWith($this->key, 'base64:')) {
239
            return base64_decode(mb_substr($this->key, 7));
240
        }
241
242
        return $this->key;
243
    }
244
245
    /**
246
     * Returns the payload string containing.
247
     *
248
     * @param \Rinvex\Fort\Contracts\CanResetPasswordContract $user
249
     * @param string                                          $email
250
     * @param int                                             $expiration
251
     *
252
     * @return string
253
     */
254
    protected function buildPayload(CanResetPasswordContract $user, $email, $expiration)
255
    {
256
        return implode(';', [
257
            $email,
258
            $expiration,
259
            $user->getKey(),
260
            $user->password,
0 ignored issues
show
Bug introduced by
Accessing password on the interface Rinvex\Fort\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...
261
        ]);
262
    }
263
264
    /**
265
     * Validate a password reset for the given credentials.
266
     *
267
     * @param array $credentials
268
     *
269
     * @return \Illuminate\Contracts\Auth\CanResetPassword|string
0 ignored issues
show
Documentation introduced by
Should the return type not be string|\Illuminate\Contracts\Auth\Authenticatable?

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...
270
     */
271
    protected function validateReset(array $credentials)
272
    {
273
        if (is_null($user = $this->getUser($credentials))) {
274
            return static::INVALID_USER;
275
        }
276
277
        if (! $this->validateNewPassword($credentials)) {
278
            return static::INVALID_PASSWORD;
279
        }
280
281
        if (! $this->validateToken($user, $credentials)) {
0 ignored issues
show
Documentation introduced by
$user is of type object<Illuminate\Contracts\Auth\Authenticatable>, but the function expects a object<Rinvex\Fort\Contr...nResetPasswordContract>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
282
            return static::INVALID_TOKEN;
283
        }
284
285
        if (! $this->validateTimestamp($credentials['expiration'])) {
286
            return static::EXPIRED_TOKEN;
287
        }
288
289
        return $user;
290
    }
291
}
292