Completed
Push — authenticator-refactor ( 7dc887...371abb )
by Simon
06:49
created

CookieAuthenticationHandler::logIn()   B

Complexity

Conditions 3
Paths 4

Size

Total Lines 35
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 25
nc 4
nop 3
dl 0
loc 35
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\Security\MemberAuthenticator;
4
5
use SilverStripe\Security\Member;
6
use SilverStripe\Control\HTTPRequest;
7
use SilverStripe\Security\AuthenticationHandler as AuthenticationHandlerInterface;
8
use SilverStripe\Security\IdentityStore;
9
use SilverStripe\Security\RememberLoginHash;
10
use SilverStripe\Security\Security;
11
use SilverStripe\ORM\FieldType\DBDatetime;
12
use SilverStripe\Control\Cookie;
13
14
/**
15
 * Authenticate a member pased on a session cookie
16
 */
17
class CookieAuthenticationHandler implements AuthenticationHandlerInterface, IdentityStore
18
{
19
20
    /**
21
     * @var string
22
     */
23
    private $deviceCookieName;
24
25
    /**
26
     * @var string
27
     */
28
    private $tokenCookieName;
29
30
    /**
31
     * @var IdentityStore
32
     */
33
    private $cascadeLogInTo;
34
35
    /**
36
     * Get the name of the cookie used to track this device
37
     *
38
     * @return string
39
     */
40
    public function getDeviceCookieName()
41
    {
42
        return $this->deviceCookieName;
43
    }
44
45
    /**
46
     * Set the name of the cookie used to track this device
47
     *
48
     * @param $deviceCookieName
49
     * @return null
50
     */
51
    public function setDeviceCookieName($deviceCookieName)
52
    {
53
        $this->deviceCookieName = $deviceCookieName;
54
    }
55
56
    /**
57
     * Get the name of the cookie used to store an login token
58
     *
59
     * @return string
60
     */
61
    public function getTokenCookieName()
62
    {
63
        return $this->tokenCookieName;
64
    }
65
66
    /**
67
     * Set the name of the cookie used to store an login token
68
     *
69
     * @param $tokenCookieName
70
     * @return null
71
     */
72
    public function setTokenCookieName($tokenCookieName)
73
    {
74
        $this->tokenCookieName = $tokenCookieName;
75
    }
76
77
    /**
78
     * Once a member is found by authenticateRequest() pass it to this identity store
79
     *
80
     * @return IdentityStore
81
     */
82
    public function getCascadeLogInTo()
83
    {
84
        return $this->cascadeLogInTo;
85
    }
86
87
    /**
88
     * Set the name of the cookie used to store an login token
89
     *
90
     * @param $cascadeLogInTo
91
     * @return null
92
     */
93
    public function setCascadeLogInTo(IdentityStore $cascadeLogInTo)
94
    {
95
        $this->cascadeLogInTo = $cascadeLogInTo;
96
    }
97
98
    /**
99
     * @param HTTPRequest $request
100
     * @return null|Member
101
     */
102
    public function authenticateRequest(HTTPRequest $request)
103
    {
104
        $uidAndToken = Cookie::get($this->getTokenCookieName());
105
        $deviceID = Cookie::get($this->getDeviceCookieName());
106
107
        // @todo Consider better placement of database_is_ready test
108
        if (!$deviceID || strpos($uidAndToken, ':') === false || !Security::database_is_ready()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $deviceID of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
109
            return null;
110
        }
111
112
        list($uid, $token) = explode(':', $uidAndToken, 2);
113
114
        if (!$uid || !$token) {
115
            return null;
116
        }
117
118
        /** @var Member $member */
119
        $member = Member::get()->byID($uid);
120
121
        /** @var RememberLoginHash $rememberLoginHash */
122
        $rememberLoginHash = null;
123
124
        // check if autologin token matches
125
        if ($member) {
126
            $hash = $member->encryptWithUserSettings($token);
127
            $rememberLoginHash = RememberLoginHash::get()
128
                ->filter(array(
129
                    'MemberID' => $member->ID,
130
                    'DeviceID' => $deviceID,
131
                    'Hash' => $hash
132
                ))->first();
133
134
            if (!$rememberLoginHash) {
135
                $member = null;
136
            } else {
137
                // Check for expired token
138
                $expiryDate = new \DateTime($rememberLoginHash->ExpiryDate);
139
                $now = DBDatetime::now();
140
                $now = new \DateTime($now->Rfc2822());
141
                if ($now > $expiryDate) {
142
                    $member = null;
143
                }
144
            }
145
        }
146
147
        if ($member) {
148
            if ($this->cascadeLogInTo) {
149
                // @todo look at how to block "regular login" triggers from happening here
150
                // @todo deal with the fact that the Session::current_session() isn't correct here :-/
151
                $this->cascadeLogInTo->logIn($member, false, $request);
152
            }
153
154
            // @todo Consider whether response should be part of logIn() as well
155
156
            // Renew the token
157
            if ($rememberLoginHash) {
158
                $rememberLoginHash->renew();
159
                $tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
160
                Cookie::set(
161
                    $this->getTokenCookieName(),
162
                    $member->ID . ':' . $rememberLoginHash->getToken(),
163
                    $tokenExpiryDays,
164
                    null,
165
                    null,
166
                    false,
167
                    true
168
                );
169
            }
170
171
            // Audit logging hook
172
            $member->extend('memberAutoLoggedIn');
173
174
            return $member;
175
        }
176
    }
177
178
    /**
179
     * @param Member $member
180
     * @param bool $persistent
181
     * @param HTTPRequest $request
182
     * @return \SilverStripe\Control\HTTPResponse|void
183
     */
184
    public function logIn(Member $member, $persistent = false, HTTPRequest $request = null)
185
    {
186
        // Cleans up any potential previous hash for this member on this device
187
        if ($alcDevice = Cookie::get($this->getDeviceCookieName())) {
188
            RememberLoginHash::get()->filter('DeviceID', $alcDevice)->removeAll();
189
        }
190
191
        // Set a cookie for persistent log-ins
192
        if ($persistent) {
193
            $rememberLoginHash = RememberLoginHash::generate($member);
194
            $tokenExpiryDays = RememberLoginHash::config()->uninherited('token_expiry_days');
195
            $deviceExpiryDays = RememberLoginHash::config()->uninherited('device_expiry_days');
196
            Cookie::set(
197
                $this->getTokenCookieName(),
198
                $member->ID . ':' . $rememberLoginHash->getToken(),
199
                $tokenExpiryDays,
200
                null,
201
                null,
202
                null,
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

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...
203
                true
204
            );
205
            Cookie::set(
206
                $this->getDeviceCookieName(),
207
                $rememberLoginHash->DeviceID,
208
                $deviceExpiryDays,
209
                null,
210
                null,
211
                null,
0 ignored issues
show
Documentation introduced by
null is of type null, but the function expects a boolean.

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...
212
                true
213
            );
214
        } else {
215
            // Clear a cookie for non-persistent log-ins
216
            $this->clearCookies();
217
        }
218
    }
219
220
    /**
221
     * @param HTTPRequest|null $request
222
     * @return \SilverStripe\Control\HTTPResponse|void
223
     */
224
    public function logOut(HTTPRequest $request = null)
225
    {
226
        $member = Security::getCurrentUser();
227
        if ($member) {
228
            RememberLoginHash::clear($member, Cookie::get('alc_device'));
229
        }
230
        $this->clearCookies();
231
232
        Security::setCurrentUser(null);
233
    }
234
235
    /**
236
     * Clear the cookies set for the user
237
     */
238
    protected function clearCookies()
239
    {
240
        Cookie::set($this->getTokenCookieName(), null);
241
        Cookie::set($this->getDeviceCookieName(), null);
242
        Cookie::force_expiry($this->getTokenCookieName());
243
        Cookie::force_expiry($this->getDeviceCookieName());
244
    }
245
}
246