PasswordResetBroker   A
last analyzed

Complexity

Total Complexity 18

Size/Duplication

Total Lines 214
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
wmc 18
lcom 1
cbo 5
dl 0
loc 214
rs 10
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A sendResetLink() 0 20 2
A reset() 0 20 2
A getUser() 0 10 3
A createToken() 0 6 1
A validateToken() 0 6 1
A validateTimestamp() 0 4 1
A getKey() 0 8 2
A buildPayload() 0 9 1
A validateReset() 0 16 4
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Rinvex\Auth\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\Auth\Contracts\CanResetPasswordContract;
14
use Rinvex\Auth\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
     * Create a new verification broker instance.
41
     *
42
     * @param \Illuminate\Contracts\Auth\UserProvider $users
43
     * @param string                                  $key
44
     * @param int                                     $expiration
45
     */
46
    public function __construct(UserProvider $users, $key, $expiration)
47
    {
48
        $this->key = $key;
49
        $this->users = $users;
50
        $this->expiration = $expiration;
51
    }
52
53
    /**
54
     * Send a password reset link to a user.
55
     *
56
     * @param array         $credentials
57
     * @param \Closure|null $callback
58
     *
59
     * @return string
60
     */
61
    public function sendResetLink(array $credentials, Closure $callback = null)
62
    {
63
        // First we will check to see if we found a user at the given credentials and
64
        // if we did not we will redirect back to this current URI with a piece of
65
        // "flash" data in the session to indicate to the developers the errors.
66
        $user = $this->getUser($credentials);
67
68
        if (is_null($user)) {
69
            return static::INVALID_USER;
70
        }
71
72
        $expiration = Carbon::now()->addMinutes($this->expiration)->timestamp;
73
74
        // Once we have the reset token, we are ready to send the message out to this
75
        // user with a link to reset their password. We will then redirect back to
76
        // the current URI having nothing set in the session to indicate errors.
77
        $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\Auth\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...
78
79
        return static::RESET_LINK_SENT;
80
    }
81
82
    /**
83
     * Reset the password for the given token.
84
     *
85
     * @param array    $credentials
86
     * @param \Closure $callback
87
     *
88
     * @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...
89
     */
90
    public function reset(array $credentials, Closure $callback)
91
    {
92
        $user = $this->validateReset($credentials);
93
94
        // If the responses from the validate method is not a user instance, we will
95
        // assume that it is a redirect and simply return it from this method and
96
        // the user is properly redirected having an error message on the post.
97
        if (! $user instanceof CanResetPasswordContract) {
98
            return $user;
99
        }
100
101
        $password = $credentials['password'];
102
103
        // Once the reset has been validated, we'll call the given callback with the
104
        // new password. This gives the user an opportunity to store the password
105
        // in their persistent storage.
106
        $callback($user, $password);
107
108
        return static::PASSWORD_RESET;
109
    }
110
111
    /**
112
     * Get the user for the given credentials.
113
     *
114
     * @param array $credentials
115
     *
116
     * @throws \UnexpectedValueException
117
     *
118
     * @return \Rinvex\Auth\Contracts\CanResetPasswordContract|null
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...
119
     */
120
    public function getUser(array $credentials): ?CanResetPasswordContract
121
    {
122
        $user = $this->users->retrieveByCredentials(Arr::only($credentials, ['email']));
123
124
        if ($user && ! $user instanceof CanResetPasswordContract) {
125
            throw new UnexpectedValueException('User must implement CanResetPassword interface.');
126
        }
127
128
        return $user;
129
    }
130
131
    /**
132
     * Create a new password reset token for the given user.
133
     *
134
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
135
     * @param int                                             $expiration
136
     *
137
     * @return string
138
     */
139
    public function createToken(CanResetPasswordContract $user, $expiration): string
140
    {
141
        $payload = $this->buildPayload($user, $user->getEmailForPasswordReset(), $expiration);
142
143
        return hash_hmac('sha256', $payload, $this->getKey());
144
    }
145
146
    /**
147
     * Validate the given password reset token.
148
     *
149
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
150
     * @param array                                           $credentials
151
     *
152
     * @return bool
153
     */
154
    public function validateToken(CanResetPasswordContract $user, array $credentials): bool
155
    {
156
        $payload = $this->buildPayload($user, $credentials['email'], $credentials['expiration']);
157
158
        return hash_equals($credentials['token'], hash_hmac('sha256', $payload, $this->getKey()));
159
    }
160
161
    /**
162
     * Validate the given expiration timestamp.
163
     *
164
     * @param int $expiration
165
     *
166
     * @return bool
167
     */
168
    public function validateTimestamp($expiration): bool
169
    {
170
        return Carbon::now()->createFromTimestamp($expiration)->isFuture();
171
    }
172
173
    /**
174
     * Return the application key.
175
     *
176
     * @return string
177
     */
178
    public function getKey(): string
179
    {
180
        if (Str::startsWith($this->key, 'base64:')) {
181
            return base64_decode(mb_substr($this->key, 7));
182
        }
183
184
        return $this->key;
185
    }
186
187
    /**
188
     * Returns the payload string containing.
189
     *
190
     * @param \Rinvex\Auth\Contracts\CanResetPasswordContract $user
191
     * @param string                                          $email
192
     * @param int                                             $expiration
193
     *
194
     * @return string
195
     */
196
    protected function buildPayload(CanResetPasswordContract $user, $email, $expiration): string
197
    {
198
        return implode(';', [
199
            $email,
200
            $expiration,
201
            $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...
202
            $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...
203
        ]);
204
    }
205
206
    /**
207
     * Validate a password reset for the given credentials.
208
     *
209
     * @param array $credentials
210
     *
211
     * @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...
212
     */
213
    protected function validateReset(array $credentials)
214
    {
215
        if (is_null($user = $this->getUser($credentials))) {
216
            return static::INVALID_USER;
217
        }
218
219
        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\Auth\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...
220
            return static::INVALID_TOKEN;
221
        }
222
223
        if (! $this->validateTimestamp($credentials['expiration'])) {
224
            return static::EXPIRED_TOKEN;
225
        }
226
227
        return $user;
228
    }
229
}
230