MediaWiki master
CookieSessionProvider.php
Go to the documentation of this file.
1<?php
24namespace MediaWiki\Session;
25
26use InvalidArgumentException;
31
39
41 protected $params = [];
42
44 protected $cookieOptions = [];
45
59 public function __construct( $params = [] ) {
60 parent::__construct();
61
62 $params += [
63 'cookieOptions' => [],
64 // @codeCoverageIgnoreStart
65 ];
66 // @codeCoverageIgnoreEnd
67
68 if ( !isset( $params['priority'] ) ) {
69 throw new InvalidArgumentException( __METHOD__ . ': priority must be specified' );
70 }
71 if ( $params['priority'] < SessionInfo::MIN_PRIORITY ||
73 ) {
74 throw new InvalidArgumentException( __METHOD__ . ': Invalid priority' );
75 }
76
77 if ( !is_array( $params['cookieOptions'] ) ) {
78 throw new InvalidArgumentException( __METHOD__ . ': cookieOptions must be an array' );
79 }
80
81 $this->priority = $params['priority'];
82 $this->cookieOptions = $params['cookieOptions'];
83 $this->params = $params;
84 unset( $this->params['priority'] );
85 unset( $this->params['cookieOptions'] );
86 }
87
88 protected function postInitSetup() {
89 $this->params += [
90 'sessionName' =>
92 ?: $this->getConfig()->get( MainConfigNames::CookiePrefix ) . '_session',
93 ];
94
95 $sameSite = $this->getConfig()->get( MainConfigNames::CookieSameSite );
96
97 // @codeCoverageIgnoreStart
98 $this->cookieOptions += [
99 // @codeCoverageIgnoreEnd
100 'prefix' => $this->getConfig()->get( MainConfigNames::CookiePrefix ),
101 'path' => $this->getConfig()->get( MainConfigNames::CookiePath ),
102 'domain' => $this->getConfig()->get( MainConfigNames::CookieDomain ),
103 'secure' => $this->getConfig()->get( MainConfigNames::CookieSecure )
104 || $this->getConfig()->get( MainConfigNames::ForceHTTPS ),
105 'httpOnly' => $this->getConfig()->get( MainConfigNames::CookieHttpOnly ),
106 'sameSite' => $sameSite,
107 ];
108 }
109
110 public function provideSessionInfo( WebRequest $request ) {
111 $sessionId = $this->getCookie( $request, $this->params['sessionName'], '' );
112 $info = [
113 'provider' => $this,
114 'forceHTTPS' => $this->getCookie( $request, 'forceHTTPS', '', false )
115 ];
116 if ( SessionManager::validateSessionId( $sessionId ) ) {
117 $info['id'] = $sessionId;
118 $info['persisted'] = true;
119 }
120
121 [ $userId, $userName, $token ] = $this->getUserInfoFromCookies( $request );
122 if ( $userId !== null ) {
123 try {
124 $userInfo = UserInfo::newFromId( $userId );
125 } catch ( InvalidArgumentException $ex ) {
126 return null;
127 }
128
129 if ( $userName !== null && $userInfo->getName() !== $userName ) {
130 $this->logger->warning(
131 'Session "{session}" requested with mismatched UserID and UserName cookies.',
132 [
133 'session' => $sessionId,
134 'mismatch' => [
135 'userid' => $userId,
136 'cookie_username' => $userName,
137 'username' => $userInfo->getName(),
138 ],
139 ] );
140 return null;
141 }
142
143 if ( $token !== null ) {
144 if ( !hash_equals( $userInfo->getToken(), $token ) ) {
145 $this->logger->warning(
146 'Session "{session}" requested with invalid Token cookie.',
147 [
148 'session' => $sessionId,
149 'userid' => $userId,
150 'username' => $userInfo->getName(),
151 ] );
152 return null;
153 }
154 $info['userInfo'] = $userInfo->verified();
155 $info['persisted'] = true; // If we have user+token, it should be
156 } elseif ( isset( $info['id'] ) ) {
157 $info['userInfo'] = $userInfo;
158 } else {
159 // No point in returning, loadSessionInfoFromStore() will
160 // reject it anyway.
161 return null;
162 }
163 } elseif ( isset( $info['id'] ) ) {
164 // No UserID cookie, so insist that the session is anonymous.
165 // Note: this event occurs for several normal activities:
166 // * anon visits Special:UserLogin
167 // * anon browsing after seeing Special:UserLogin
168 // * anon browsing after edit or preview
169 $this->logger->debug(
170 'Session "{session}" requested without UserID cookie',
171 [
172 'session' => $info['id'],
173 ] );
174 $info['userInfo'] = UserInfo::newAnonymous();
175 } else {
176 // No session ID and no user is the same as an empty session, so
177 // there's no point.
178 return null;
179 }
180
181 return new SessionInfo( $this->priority, $info );
182 }
183
184 public function persistsSessionId() {
185 return true;
186 }
187
188 public function canChangeUser() {
189 return true;
190 }
191
192 public function persistSession( SessionBackend $session, WebRequest $request ) {
193 $response = $request->response();
194 if ( $response->headersSent() ) {
195 // Can't do anything now
196 $this->logger->debug( __METHOD__ . ': Headers already sent' );
197 return;
198 }
199
200 $user = $session->getUser();
201
202 $cookies = $this->cookieDataToExport( $user, $session->shouldRememberUser() );
203 $sessionData = $this->sessionDataToExport( $user );
204
205 $options = $this->cookieOptions;
206
207 $forceHTTPS = $session->shouldForceHTTPS() || $user->requiresHTTPS();
208 if ( $forceHTTPS ) {
209 $options['secure'] = $this->getConfig()->get( MainConfigNames::CookieSecure )
210 || $this->getConfig()->get( MainConfigNames::ForceHTTPS );
211 }
212
213 $response->setCookie( $this->params['sessionName'], $session->getId(), null,
214 [ 'prefix' => '' ] + $options
215 );
216
217 foreach ( $cookies as $key => $value ) {
218 if ( $value === false ) {
219 $response->clearCookie( $key, $options );
220 } else {
221 $expirationDuration = $this->getLoginCookieExpiration( $key, $session->shouldRememberUser() );
222 $expiration = $expirationDuration ? $expirationDuration + time() : null;
223 $response->setCookie( $key, (string)$value, $expiration, $options );
224 }
225 }
226
227 $this->setForceHTTPSCookie( $forceHTTPS, $session, $request );
228 $this->setLoggedOutCookie( $session->getLoggedOutTimestamp(), $request );
229
230 if ( $sessionData ) {
231 $session->addData( $sessionData );
232 }
233 }
234
235 public function unpersistSession( WebRequest $request ) {
236 $response = $request->response();
237 if ( $response->headersSent() ) {
238 // Can't do anything now
239 $this->logger->debug( __METHOD__ . ': Headers already sent' );
240 return;
241 }
242
243 $cookies = [
244 'UserID' => false,
245 'Token' => false,
246 ];
247
248 $response->clearCookie(
249 $this->params['sessionName'], [ 'prefix' => '' ] + $this->cookieOptions
250 );
251
252 foreach ( $cookies as $key => $value ) {
253 $response->clearCookie( $key, $this->cookieOptions );
254 }
255
256 $this->setForceHTTPSCookie( false, null, $request );
257 }
258
266 protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
267 if ( $this->getConfig()->get( MainConfigNames::ForceHTTPS ) ) {
268 // No need to send a cookie if the wiki is always HTTPS (T256095)
269 return;
270 }
271 $response = $request->response();
272 if ( $set ) {
273 if ( $backend->shouldRememberUser() ) {
274 $expirationDuration = $this->getLoginCookieExpiration(
275 'forceHTTPS',
276 true
277 );
278 $expiration = $expirationDuration ? $expirationDuration + time() : null;
279 } else {
280 $expiration = null;
281 }
282 $response->setCookie( 'forceHTTPS', 'true', $expiration,
283 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
284 } else {
285 $response->clearCookie( 'forceHTTPS',
286 [ 'prefix' => '', 'secure' => false ] + $this->cookieOptions );
287 }
288 }
289
294 protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
295 if ( $loggedOut + 86400 > time() &&
296 $loggedOut !== (int)$this->getCookie( $request, 'LoggedOut', $this->cookieOptions['prefix'] )
297 ) {
298 $request->response()->setCookie( 'LoggedOut', (string)$loggedOut, $loggedOut + 86400,
299 $this->cookieOptions );
300 }
301 }
302
303 public function getVaryCookies() {
304 return [
305 // Vary on token and session because those are the real authn
306 // determiners. UserID and UserName don't matter without those.
307 $this->cookieOptions['prefix'] . 'Token',
308 $this->cookieOptions['prefix'] . 'LoggedOut',
309 $this->params['sessionName'],
310 'forceHTTPS',
311 ];
312 }
313
314 public function suggestLoginUsername( WebRequest $request ) {
315 $name = $this->getCookie( $request, 'UserName', $this->cookieOptions['prefix'] );
316 if ( $name !== null ) {
317 if ( $this->userNameUtils->isTemp( $name ) ) {
318 $name = false;
319 } else {
320 $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
321 }
322 }
323 return $name === false ? null : $name;
324 }
325
331 protected function getUserInfoFromCookies( $request ) {
332 $prefix = $this->cookieOptions['prefix'];
333 return [
334 $this->getCookie( $request, 'UserID', $prefix ),
335 $this->getCookie( $request, 'UserName', $prefix ),
336 $this->getCookie( $request, 'Token', $prefix ),
337 ];
338 }
339
348 protected function getCookie( $request, $key, $prefix, $default = null ) {
349 $value = $request->getCookie( $key, $prefix, $default );
350 if ( $value === 'deleted' ) {
351 // PHP uses this value when deleting cookies. A legitimate cookie will never have
352 // this value (usernames start with uppercase, token is longer, other auth cookies
353 // are booleans or integers). Seeing this means that in a previous request we told the
354 // client to delete the cookie, but it has poor cookie handling. Pretend the cookie is
355 // not there to avoid invalidating the session.
356 return null;
357 }
358 return $value;
359 }
360
367 protected function cookieDataToExport( $user, $remember ) {
368 if ( $user->isAnon() ) {
369 return [
370 'UserID' => false,
371 'Token' => false,
372 ];
373 } else {
374 return [
375 'UserID' => $user->getId(),
376 'UserName' => $user->getName(),
377 'Token' => $remember ? (string)$user->getToken() : false,
378 ];
379 }
380 }
381
387 protected function sessionDataToExport( $user ) {
388 return [];
389 }
390
391 public function whyNoSession() {
392 return wfMessage( 'sessionprovider-nocookies' );
393 }
394
395 public function getRememberUserDuration() {
396 return min( $this->getLoginCookieExpiration( 'UserID', true ),
397 $this->getLoginCookieExpiration( 'Token', true ) ) ?: null;
398 }
399
406 protected function getExtendedLoginCookies() {
407 return [ 'UserID', 'UserName', 'Token' ];
408 }
409
420 protected function getLoginCookieExpiration( $cookieName, $shouldRememberUser ) {
421 $extendedCookies = $this->getExtendedLoginCookies();
422 $normalExpiration = $this->getConfig()->get( MainConfigNames::CookieExpiration );
423
424 if ( $shouldRememberUser && in_array( $cookieName, $extendedCookies, true ) ) {
425 $extendedExpiration = $this->getConfig()->get( MainConfigNames::ExtendedLoginCookieExpiration );
426
427 return ( $extendedExpiration !== null ) ? (int)$extendedExpiration : (int)$normalExpiration;
428 } else {
429 return (int)$normalExpiration;
430 }
431 }
432}
wfMessage( $key,... $params)
This is the function for getting translated interface messages.
A class containing constants representing the names of configuration variables.
const ForceHTTPS
Name constant for the ForceHTTPS setting, for use with Config::get()
const CookieExpiration
Name constant for the CookieExpiration setting, for use with Config::get()
const CookieDomain
Name constant for the CookieDomain setting, for use with Config::get()
const CookiePath
Name constant for the CookiePath setting, for use with Config::get()
const CookieSameSite
Name constant for the CookieSameSite setting, for use with Config::get()
const CookieSecure
Name constant for the CookieSecure setting, for use with Config::get()
const SessionName
Name constant for the SessionName setting, for use with Config::get()
const ExtendedLoginCookieExpiration
Name constant for the ExtendedLoginCookieExpiration setting, for use with Config::get()
const CookiePrefix
Name constant for the CookiePrefix setting, for use with Config::get()
const CookieHttpOnly
Name constant for the CookieHttpOnly setting, for use with Config::get()
The WebRequest class encapsulates getting at data passed in the URL or via a POSTed form,...
response()
Return a handle to WebResponse style object, for setting cookies, headers and other stuff,...
A CookieSessionProvider persists sessions using cookies.
suggestLoginUsername(WebRequest $request)
Get a suggested username for the login form.
canChangeUser()
Indicate whether the user associated with the request can be changed.
sessionDataToExport( $user)
Return extra data to store in the session.
persistSession(SessionBackend $session, WebRequest $request)
Persist a session into a request/response.
getUserInfoFromCookies( $request)
Fetch the user identity from cookies.
whyNoSession()
Return a Message for why sessions might not be being persisted.For example, "check whether you're blo...
setLoggedOutCookie( $loggedOut, WebRequest $request)
getExtendedLoginCookies()
Gets the list of cookies that must be set to the 'remember me' duration, if $wgExtendedLoginCookieExp...
setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request)
Set the "forceHTTPS" cookie, unless $wgForceHTTPS prevents it.
getVaryCookies()
Return the list of cookies that need varying on.
getCookie( $request, $key, $prefix, $default=null)
Get a cookie.
persistsSessionId()
Indicate whether self::persistSession() can save arbitrary session IDs.
provideSessionInfo(WebRequest $request)
Provide session info for a request.
cookieDataToExport( $user, $remember)
Return the data to store in cookies.
getRememberUserDuration()
Returns the duration (in seconds) for which users will be remembered when Session::setRememberUser() ...
unpersistSession(WebRequest $request)
Remove any persisted session from a request/response.
getLoginCookieExpiration( $cookieName, $shouldRememberUser)
Returns the lifespan of the login cookies, in seconds.
postInitSetup()
A provider can override this to do any necessary setup after init() is called.
This is the actual workhorse for Session.
addData(array $newData)
Add data to the session.
shouldForceHTTPS()
Whether HTTPS should be forced.
getId()
Returns the session ID.
getLoggedOutTimestamp()
Fetch the "logged out" timestamp.
getUser()
Returns the authenticated user for this session.
shouldRememberUser()
Indicate whether the user should be remembered independently of the session ID.
Value object returned by SessionProvider.
const MIN_PRIORITY
Minimum allowed priority.
const MAX_PRIORITY
Maximum allowed priority.
static validateSessionId( $id)
Validate a session ID.
A SessionProvider provides SessionInfo and support for Session.
static newAnonymous()
Create an instance for an anonymous (i.e.
Definition UserInfo.php:79
static newFromId( $id, $verified=false)
Create an instance for a logged-in user by ID.
Definition UserInfo.php:89
internal since 1.36
Definition User.php:93
Shared interface for rigor levels when dealing with User methods.