EmailVerificationBroker::validateToken()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 2
dl 0
loc 6
rs 10
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 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\CanVerifyEmailContract;
14
use Rinvex\Auth\Contracts\EmailVerificationBrokerContract;
15
16
class EmailVerificationBroker implements EmailVerificationBrokerContract
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
     * {@inheritdoc}
55
     */
56
    public function sendVerificationLink(array $credentials): string
57
    {
58
        // First we will check to see if we found a user at the given credentials and
59
        // if we did not we will redirect back to this current URI with a piece of
60
        // "flash" data in the session to indicate to the developers the errors.
61
        if (is_null($user = $this->getUser($credentials))) {
62
            return static::INVALID_USER;
63
        }
64
65
        $expiration = Carbon::now()->addMinutes($this->expiration)->timestamp;
66
67
        // Once we have the verification token, we are ready to send the message out to
68
        // this user with a link to verify their email. We will then redirect back to
69
        // the current URI having nothing set in the session to indicate any errors
70
        $user->sendEmailVerificationNotification($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...CanVerifyEmailContract>.

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...
71
72
        return static::LINK_SENT;
73
    }
74
75
    /**
76
     * {@inheritdoc}
77
     */
78
    public function verify(array $credentials, Closure $callback)
79
    {
80
        // If the responses from the validate method is not a user instance, we will
81
        // assume that it is a redirect and simply return it from this method and
82
        // the user is properly redirected having an error message on the post.
83
        $user = $this->validateVerification($credentials);
84
85
        if (! $user instanceof CanVerifyEmailContract) {
86
            return $user;
87
        }
88
89
        // Once the email has been verified, we'll call the given
90
        // callback, then we'll delete the token and return.
91
        // in their persistent storage.
92
        $callback($user);
93
94
        return static::EMAIL_VERIFIED;
95
    }
96
97
    /**
98
     * Get the user for the given credentials.
99
     *
100
     * @param array $credentials
101
     *
102
     * @throws \UnexpectedValueException
103
     *
104
     * @return \Rinvex\Auth\Contracts\CanVerifyEmailContract|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...
105
     */
106
    public function getUser(array $credentials): ?CanVerifyEmailContract
107
    {
108
        $user = $this->users->retrieveByCredentials(Arr::only($credentials, ['email']));
109
110
        if ($user && ! $user instanceof CanVerifyEmailContract) {
111
            throw new UnexpectedValueException('User must implement CanVerifyEmailContract interface.');
112
        }
113
114
        return $user;
115
    }
116
117
    /**
118
     * Create a new email verification token for the given user.
119
     *
120
     * @param \Rinvex\Auth\Contracts\CanVerifyEmailContract $user
121
     * @param int                                           $expiration
122
     *
123
     * @return string
124
     */
125
    public function createToken(CanVerifyEmailContract $user, $expiration): string
126
    {
127
        $payload = $this->buildPayload($user, $user->getEmailForVerification(), $expiration);
128
129
        return hash_hmac('sha256', $payload, $this->getKey());
130
    }
131
132
    /**
133
     * Validate the given email verification token.
134
     *
135
     * @param \Rinvex\Auth\Contracts\CanVerifyEmailContract $user
136
     * @param array                                         $credentials
137
     *
138
     * @return bool
139
     */
140
    public function validateToken(CanVerifyEmailContract $user, array $credentials): bool
141
    {
142
        $payload = $this->buildPayload($user, $credentials['email'], $credentials['expiration']);
143
144
        return hash_equals($credentials['token'], hash_hmac('sha256', $payload, $this->getKey()));
145
    }
146
147
    /**
148
     * Validate the given expiration timestamp.
149
     *
150
     * @param int $expiration
151
     *
152
     * @return bool
153
     */
154
    public function validateTimestamp($expiration): bool
155
    {
156
        return Carbon::now()->createFromTimestamp($expiration)->isFuture();
157
    }
158
159
    /**
160
     * Return the application key.
161
     *
162
     * @return string
163
     */
164
    public function getKey(): string
165
    {
166
        if (Str::startsWith($this->key, 'base64:')) {
167
            return base64_decode(mb_substr($this->key, 7));
168
        }
169
170
        return $this->key;
171
    }
172
173
    /**
174
     * Returns the payload string containing.
175
     *
176
     * @param \Rinvex\Auth\Contracts\CanVerifyEmailContract $user
177
     * @param string                                        $email
178
     * @param int                                           $expiration
179
     *
180
     * @return string
181
     */
182
    protected function buildPayload(CanVerifyEmailContract $user, $email, $expiration): string
183
    {
184
        return implode(';', [
185
            $email,
186
            $expiration,
187
            $user->getKey(),
0 ignored issues
show
Bug introduced by
The method getKey() does not seem to exist on object<Rinvex\Auth\Contr...CanVerifyEmailContract>.

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...
188
            $user->password,
0 ignored issues
show
Bug introduced by
Accessing password on the interface Rinvex\Auth\Contracts\CanVerifyEmailContract 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...
189
        ]);
190
    }
191
192
    /**
193
     * Validate an email verification for the given credentials.
194
     *
195
     * @param array $credentials
196
     *
197
     * @return \Rinvex\Auth\Contracts\CanVerifyEmailContract|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...
198
     */
199
    protected function validateVerification(array $credentials)
200
    {
201
        if (is_null($user = $this->getUser($credentials))) {
202
            return static::INVALID_USER;
203
        }
204
205
        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...CanVerifyEmailContract>.

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...
206
            return static::INVALID_TOKEN;
207
        }
208
209
        if (! $this->validateTimestamp($credentials['expiration'])) {
210
            return static::EXPIRED_TOKEN;
211
        }
212
213
        return $user;
214
    }
215
}
216