Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
14.96% covered (danger)
14.96%
41 / 274
15.79% covered (danger)
15.79%
3 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthSessionProvider
14.96% covered (danger)
14.96%
41 / 274
15.79% covered (danger)
15.79%
3 / 19
4959.75
0.00% covered (danger)
0.00%
0 / 1
 __construct
76.92% covered (warning)
76.92%
10 / 13
0.00% covered (danger)
0.00%
0 / 1
2.05
 postInitSetup
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
2
 returnParentSessionInfo
0.00% covered (danger)
0.00%
0 / 19
0.00% covered (danger)
0.00%
0 / 1
12
 provideSessionInfo
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
552
 refreshSessionInfo
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
110
 sessionIdWasReset
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 sessionDataToExport
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
6
 cookieDataToExport
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
12
 persistSession
0.00% covered (danger)
0.00%
0 / 45
0.00% covered (danger)
0.00%
0 / 1
240
 unpersistSession
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
12
 invalidateSessionsForUser
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
20
 preventSessionsForUser
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
30
 setForceHTTPSCookie
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 setLoggedOutCookie
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 getVaryCookies
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
6
 suggestLoginUsername
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
5.05
 getCentralCookieDomain
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getExtendedLoginCookies
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 getRememberUserDuration
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3use MediaWiki\Context\RequestContext;
4use MediaWiki\Extension\CentralAuth\CentralAuthSessionManager;
5use MediaWiki\Extension\CentralAuth\Config\CAMainConfigNames;
6use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
7use MediaWiki\MainConfigNames;
8use MediaWiki\Password\InvalidPassword;
9use MediaWiki\Password\PasswordError;
10use MediaWiki\Password\PasswordFactory;
11use MediaWiki\Request\FauxRequest;
12use MediaWiki\Request\WebRequest;
13use MediaWiki\Session\CookieSessionProvider;
14use MediaWiki\Session\SessionBackend;
15use MediaWiki\Session\SessionInfo;
16use MediaWiki\Session\SessionManager;
17use MediaWiki\Session\UserInfo;
18use MediaWiki\User\TempUser\TempUserConfig;
19use MediaWiki\User\User;
20use MediaWiki\User\UserIdentityLookup;
21use MediaWiki\User\UserRigorOptions;
22use Wikimedia\Rdbms\IDBAccessObject;
23
24/**
25 * CentralAuth cookie-based sessions.
26 *
27 * This is intended to completely replace the core CookieSessionProvider.
28 *
29 * @warning Due to the complicated way CentralAuth has historically interacted with core
30 *  sessions, this is somewhat complicated and is probably not a good example to
31 *  copy if you're writing your own SessionProvider.
32 */
33class CentralAuthSessionProvider extends CookieSessionProvider {
34
35    /** @var bool */
36    protected $enable = false;
37
38    /** @var array */
39    protected $centralCookieOptions = [];
40
41    private UserIdentityLookup $userIdentityLookup;
42    private CentralAuthSessionManager $sessionManager;
43    private TempUserConfig $tempUserConfig;
44
45    /**
46     * @param TempUserConfig $tempUserConfig
47     * @param UserIdentityLookup $userIdentityLookup
48     * @param CentralAuthSessionManager $sessionManager
49     * @param array $params In addition to the parameters for
50     * CookieSessionProvider, the following are
51     * recognized:
52     *  - enable: Whether to set CentralAuth-specific features. Defaults to
53     *    $wgCentralAuthCookies.
54     *  - centralSessionName: Central session cookie name. Defaults to
55     *    centralCookieOptions['prefix'] . 'Session'. Note this does not
56     *    replace the parent class's 'sessionName', it's a different cookie.
57     *  - centralCookieOptions: Settings for central cookies
58     *     - prefix: Cookie prefix, defaults to $wgCentralAuthCookiePrefix
59     *     - path: Cookie path, defaults to $wgCentralAuthCookiePath
60     *     - domain: Cookie domain, defaults to $wgCentralAuthCookieDomain
61     *     - secure: Cookie secure flag, defaults to $wgCookieSecure
62     *     - httpOnly: Cookie httpOnly flag, defaults to $wgCookieHttpOnly
63     *     - sameSite: Cookie SameSite attribute, defaults to $wgCookieSameSite
64     */
65    public function __construct(
66        TempUserConfig $tempUserConfig,
67        UserIdentityLookup $userIdentityLookup,
68        CentralAuthSessionManager $sessionManager,
69        $params = []
70    ) {
71        $this->userIdentityLookup = $userIdentityLookup;
72        $this->sessionManager = $sessionManager;
73        $this->tempUserConfig = $tempUserConfig;
74
75        $params += [
76            'centralCookieOptions' => [],
77        ];
78
79        if ( !is_array( $params['centralCookieOptions'] ) ) {
80            throw new \InvalidArgumentException(
81                __METHOD__ . ': centralCookieOptions must be an array'
82            );
83        }
84
85        $this->centralCookieOptions = $params['centralCookieOptions'];
86        unset( $params['centralCookieOptions'] );
87
88        parent::__construct( $params );
89    }
90
91    protected function postInitSetup() {
92        parent::postInitSetup();
93
94        $this->centralCookieOptions += [
95            'prefix' => $this->getConfig()->get( CAMainConfigNames::CentralAuthCookiePrefix ),
96            'path' => $this->getConfig()->get( CAMainConfigNames::CentralAuthCookiePath ),
97            'domain' => $this->getConfig()->get( CAMainConfigNames::CentralAuthCookieDomain ),
98            'secure' => $this->getConfig()->get( MainConfigNames::CookieSecure ) ||
99                $this->getConfig()->get( MainConfigNames::ForceHTTPS ),
100            'httpOnly' => $this->getConfig()->get( MainConfigNames::CookieHttpOnly ),
101            'sameSite' => $this->getConfig()->get( MainConfigNames::CookieSameSite )
102        ];
103
104        $params = [
105            'enable' => $this->getConfig()->get( CAMainConfigNames::CentralAuthCookies ),
106            'centralSessionName' => $this->centralCookieOptions['prefix'] . 'Session',
107        ];
108        $this->params += $params;
109
110        $this->enable = (bool)$params['enable'];
111    }
112
113    /**
114     * Get the local session info, with CentralAuthSource metadata.
115     *
116     * @param WebRequest $request
117     * @param bool $forceEmptyPersist If this is true and there is no local
118     *   session, return a persistent empty local session to override the
119     *   non-functional CentralAuth one. This prevents the deletion of global
120     *   cookies, allowing the user to retain their central login elsewhere
121     *   in the same cookie domain. It makes sense to do this for problems with
122     *   the central session that are specific to the local wiki. (T342475)
123     * @return SessionInfo|null
124     */
125    private function returnParentSessionInfo( WebRequest $request, $forceEmptyPersist = false ) {
126        $info = parent::provideSessionInfo( $request );
127        if ( $info ) {
128            return new SessionInfo( $info->getPriority(), [
129                'copyFrom' => $info,
130                'metadata' => [
131                    'CentralAuthSource' => 'Local',
132                ],
133            ] );
134        }
135
136        if ( $forceEmptyPersist ) {
137            return new SessionInfo( $this->priority, [
138                'id' => null,
139                'provider' => $this,
140                'idIsSafe' => true,
141                'persisted' => true,
142                'metadata' => [
143                    'CentralAuthSource' => 'Local',
144                ],
145            ] );
146        }
147
148        return null;
149    }
150
151    /**
152     * Determine whether $request has a valid CentralAuth session.
153     *
154     * The following requests are considered to have a valid CentralAuth session:
155     * - A request with a centralauth_User and centralauth_Token cookie (prefix is configurable)
156     *   that match the data in the globaluser table.
157     * - A request with a centralauth_Session cookie where the referenced data exists in the
158     *   central session store, and the username and token in the central session object match
159     *   the data in the globaluser table.
160     *
161     * The following requests are considered to have a valid local session (marked with
162     * CentralAuthSource => Local in the session metadata):
163     * - A request with a centralauth_Session cookie where the referenced data exists in the
164     *   central session store, but is a stub (has pending_name or pending_guid fields).
165     * - Anything that CookieSessionProvider considers valid, if the session is anonymous or for
166     *   a local user that's not attached to the global CentralAuth user.
167     * - When $wgCentralAuthCookies is disabled, falls back to CookieSessionProvider entirely.
168     *
169     * @param WebRequest $request
170     * @return SessionInfo|null
171     */
172    public function provideSessionInfo( WebRequest $request ) {
173        if ( !$this->enable ) {
174            $this->logger->debug( __METHOD__ . ': Not enabled, falling back to core sessions' );
175            return $this->returnParentSessionInfo( $request );
176        }
177
178        $info = [
179            'id' => $this->getCookie( $request, $this->params['sessionName'], '' )
180        ];
181        if ( !SessionManager::validateSessionId( $info['id'] ) ) {
182            unset( $info['id'] );
183        }
184
185        $userName = null;
186        $token = null;
187        $from = null;
188
189        $prefix = $this->centralCookieOptions['prefix'];
190        $userCookie = $this->getCookie( $request, 'User', $prefix );
191        $tokenCookie = $this->getCookie( $request, 'Token', $prefix );
192        if ( $userCookie !== null && $tokenCookie !== null ) {
193            $userName = $userCookie;
194            $token = $tokenCookie;
195            $from = 'cookies';
196        } else {
197            $id = $this->getCookie( $request, $this->params['centralSessionName'], '' );
198            if ( $id !== null ) {
199                $data = $this->sessionManager->getCentralSessionById( $id );
200                if ( isset( $data['pending_name'] ) || isset( $data['pending_guid'] ) ) {
201                    $this->logger->debug( __METHOD__ . ': uninitialized session' );
202                } elseif ( isset( $data['token'] ) && isset( $data['user'] ) ) {
203                    $token = $data['token'];
204                    $userName = $data['user'];
205                    $from = 'session';
206                } else {
207                    $this->logger->debug( __METHOD__ . ': uninitialized session' );
208                }
209            }
210        }
211        if ( $userName === null || $token === null ) {
212            return $this->returnParentSessionInfo( $request );
213        }
214
215        // Check to avoid session ID collisions, as reported on T21158
216        if ( $userCookie === null ) {
217            $this->logger->debug(
218                __METHOD__ . ': no User cookie, so unable to check for session mismatch'
219            );
220            return $this->returnParentSessionInfo( $request );
221        }
222
223        if ( $userCookie != $userName ) {
224            $this->logger->debug(
225                __METHOD__ . ': Session ID/User mismatch. Possible session collision. ' .
226                    "Expected: $userName; actual: $userCookie"
227            );
228            return $this->returnParentSessionInfo( $request );
229        }
230
231        // Clean up username
232        $userName = $this->userNameUtils->getCanonical( $userName );
233        if ( !$userName ) {
234            $this->logger->debug( __METHOD__ . ': invalid username' );
235            return $this->returnParentSessionInfo( $request );
236        }
237        if ( !$this->userNameUtils->isUsable( $userName ) ) {
238            // Log a warning if the username is not usable, except if the name is reserved by the temporary account
239            // system to avoid spamming the logs (T373827).
240            if ( !$this->tempUserConfig->isReservedName( $userName ) ) {
241                $this->logger->warning(
242                    __METHOD__ . ': username {username} is not usable on this wiki', [
243                        'username' => $userName,
244                    ]
245                );
246            }
247            return $this->returnParentSessionInfo( $request, true );
248        }
249
250        // Try the central user
251        $centralUser = CentralAuthUser::getInstanceByName( $userName );
252
253        // Skip if they're being renamed
254        if ( $centralUser->renameInProgress() ) {
255            $this->logger->debug( __METHOD__ . ': rename in progress' );
256            // No fallback here, just fail it because our SessionCheckMetadata
257            // hook will do so anyway.
258            return null;
259        }
260
261        if ( !$centralUser->exists() ) {
262            $this->logger->debug( __METHOD__ . ': global account doesn\'t exist' );
263            return $this->returnParentSessionInfo( $request );
264        }
265        if ( !$centralUser->isAttached() ) {
266            $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $userName );
267            if ( $userIdentity && $userIdentity->isRegistered() ) {
268                $this->logger->debug( __METHOD__ . ': not attached and local account exists' );
269                return $this->returnParentSessionInfo( $request, true );
270            }
271        }
272        if ( $centralUser->authenticateWithToken( $token ) != 'ok' ) {
273            $this->logger->debug( __METHOD__ . ': token mismatch' );
274            // At this point, don't log in with a local session anymore
275            return null;
276        }
277
278        $this->logger->debug( __METHOD__ . ": logged in from $from" );
279
280        $info += [
281            'userInfo' => UserInfo::newFromName( $userName, true ),
282            'provider' => $this,
283            // CA sessions are always persistent
284            'persisted' => true,
285            'remembered' => $tokenCookie !== null,
286            'metadata' => [
287                'CentralAuthSource' => 'CentralAuth',
288            ],
289        ];
290
291        return new SessionInfo( $this->priority, $info );
292    }
293
294    /** @inheritDoc */
295    public function refreshSessionInfo( SessionInfo $info, WebRequest $request, &$metadata ) {
296        // Check on the metadata, to avoid T124409
297        if ( isset( $metadata['CentralAuthSource'] ) ) {
298            $name = $info->getUserInfo()->getName();
299            if ( $name === null ) {
300                return true;
301            }
302
303            $source = 'Local';
304            if ( $this->enable ) {
305                $centralUser = CentralAuthUser::getInstanceByName( $name );
306                $centralUserExists = $centralUser->exists();
307                if ( $centralUserExists && $centralUser->isAttached() ) {
308                    $source = 'CentralAuth';
309                } elseif ( $centralUserExists ) {
310                    $userIdentity = $this->userIdentityLookup->getUserIdentityByName(
311                        $name,
312                        IDBAccessObject::READ_LATEST
313                    );
314                    if ( !$userIdentity || !$userIdentity->isRegistered() ) {
315                        $source = 'CentralAuth';
316                    }
317                }
318            }
319            if ( $metadata['CentralAuthSource'] !== $source ) {
320                $this->logger->warning(
321                    'Session "{session}": CentralAuth saved source {saved} != expected source {expected}', [
322                        'session' => $info->__toString(),
323                        'saved' => $metadata['CentralAuthSource'],
324                        'expected' => $source,
325                    ]
326                );
327
328                return false;
329            }
330        }
331
332        return true;
333    }
334
335    /** @inheritDoc */
336    public function sessionIdWasReset( SessionBackend $session, $oldId ) {
337        if ( !$this->enable ) {
338            return;
339        }
340
341        // We need a Session to pass to CentralAuthSessionManager::setCentralSession()
342        // to reset the session ID, so create one on a new FauxRequest.
343        $s = $session->getSession( new FauxRequest() );
344
345        // We also need to fetch the current central data to pass to
346        // CentralAuthSessionManager::setCentralSession() when resetting the ID.
347        $data = $this->sessionManager->getCentralSession( $s );
348
349        $this->sessionManager->setCentralSession( $data, true, $s );
350    }
351
352    /** @inheritDoc */
353    protected function sessionDataToExport( $user ) {
354        $data = parent::sessionDataToExport( $user );
355
356        // CentralAuth needs to prevent core login-from-session to
357        // avoid bugs like T124409
358        $centralUser = CentralAuthUser::getInstance( $user );
359        if ( $centralUser->isAttached() ) {
360            unset( $data['wsToken'] );
361        }
362
363        return $data;
364    }
365
366    /** @inheritDoc */
367    protected function cookieDataToExport( $user, $remember ) {
368        // If we're going to set CA cookies, don't remember in core cookies.
369        if ( $this->enable && $remember ) {
370            $centralUser = CentralAuthUser::getInstance( $user );
371            $remember = !$centralUser->isAttached();
372        }
373
374        return parent::cookieDataToExport( $user, $remember );
375    }
376
377    /**
378     * Sets CentralAuth and core authentication cookies.
379     *
380     * - For authenticated sessions where the local account is owned by the corresponding central
381     *   account, the core <wiki>UserToken cookie is not set, the centralauth_Session,
382     *   centralauth_User, and (if the session is remembered) centralauth_Token cookies are set,
383     *   and a new central session object is created in the central session store if there wasn't
384     *   already a centralauth_Session cookie pointing at a valid object.
385     * - Otherwise (including stub central sessions), behavior is identical to CookieSessionProvider.
386     *
387     * @param SessionBackend $session
388     * @param WebRequest $request
389     */
390    public function persistSession( SessionBackend $session, WebRequest $request ) {
391        parent::persistSession( $session, $request );
392
393        if ( !$this->enable ) {
394            return;
395        }
396
397        $response = $request->response();
398        if ( $response->headersSent() ) {
399            // Can't do anything now
400            return;
401        }
402
403        $s = $session->getSession( $request );
404
405        $user = $session->getUser();
406        $centralUser = CentralAuthUser::getInstance( $user );
407
408        if ( $centralUser->exists() && ( $centralUser->isAttached() || !$user->getId() ) ) {
409            // CentralAuth needs to prevent core login-from-session to
410            // avoid bugs like T124409
411            $data = &$session->getData();
412            if ( array_key_exists( 'wsToken', $data ) ) {
413                unset( $data['wsToken'] );
414                $session->dirty();
415            }
416            unset( $data );
417
418            $metadata = $session->getProviderMetadata();
419            $metadata['CentralAuthSource'] = 'CentralAuth';
420            $session->setProviderMetadata( $metadata );
421
422            $remember = $session->shouldRememberUser();
423
424            $options = $this->centralCookieOptions;
425
426            // We only save the user into the central session if it's not a
427            // "pending" session, but we still need the ID to set the cookie.
428            $centralSessionId = $s->get( 'CentralAuth::centralSessionId' )
429                ?: $this->getCookie( $request, $this->params['centralSessionName'], '' )
430                ?: false;
431            $data = $centralSessionId ? $this->sessionManager->getCentralSessionById( $centralSessionId ) : [];
432            if ( isset( $data['pending_name'] ) ) {
433                $remember = false;
434            } else {
435                $data['user'] = $centralUser->getName();
436                $data['token'] = $centralUser->getAuthToken();
437                $data['remember'] = $remember;
438            }
439            $centralSessionId = $this->sessionManager->setCentralSession( $data, $centralSessionId, $s );
440
441            $cookies = [
442                'User' => (string)$centralUser->getName(),
443                'Token' => $remember ? (string)$centralUser->getAuthToken() : false,
444            ];
445            foreach ( $cookies as $name => $value ) {
446                if ( $value === false ) {
447                    $response->clearCookie( $name, $options );
448                } else {
449                    $expirationDuration = $this->getLoginCookieExpiration( $name, $remember );
450                    $expiration = $expirationDuration ? $expirationDuration + time() : null;
451                    $response->setCookie( $name, (string)$value, $expiration, $options );
452                }
453            }
454
455            $response->setCookie( $this->params['centralSessionName'], $centralSessionId, null,
456                [ 'prefix' => '' ] + $options );
457        } else {
458            $metadata = $session->getProviderMetadata();
459            $metadata['CentralAuthSource'] = 'Local';
460            $session->setProviderMetadata( $metadata );
461        }
462    }
463
464    /**
465     * Deletes CentralAuth and core authentication cookies.
466     *
467     * @param WebRequest $request
468     */
469    public function unpersistSession( WebRequest $request ) {
470        parent::unpersistSession( $request );
471
472        if ( !$this->enable ) {
473            return;
474        }
475
476        $response = $request->response();
477        if ( $response->headersSent() ) {
478            // Can't do anything now
479            $this->logger->debug( __METHOD__ . ': Headers already sent' );
480            return;
481        }
482
483        $expiry = time() - 86400;
484        $response->clearCookie( 'User', $this->centralCookieOptions );
485        $response->clearCookie( 'Token', $this->centralCookieOptions );
486        $response->clearCookie( $this->params['centralSessionName'],
487            [ 'prefix' => '' ] + $this->centralCookieOptions );
488    }
489
490    /** @inheritDoc */
491    public function invalidateSessionsForUser( User $user ) {
492        $centralUser = CentralAuthUser::getPrimaryInstance( $user );
493        if ( $centralUser->exists() && ( $centralUser->isAttached() || !$user->isRegistered() ) ) {
494            $centralUser->resetAuthToken();
495        }
496    }
497
498    /** @inheritDoc */
499    public function preventSessionsForUser( $username ) {
500        $username = $this->userNameUtils->getCanonical( $username );
501        if ( !$username ) {
502            return;
503        }
504
505        $centralUser = CentralAuthUser::getPrimaryInstanceByName( $username );
506        if ( !$centralUser->exists() ) {
507            return;
508        }
509
510        // Reset the user's password to something invalid and reset the token
511        // if it's not already invalid.
512        $config = RequestContext::getMain()->getConfig();
513        $passwordFactory = new PasswordFactory(
514            $config->get( MainConfigNames::PasswordConfig ),
515            $config->get( MainConfigNames::PasswordDefault )
516        );
517
518        try {
519            $password = $passwordFactory->newFromCiphertext( $centralUser->getPassword() );
520        } catch ( PasswordError $e ) {
521            return;
522        }
523        if ( !$password instanceof InvalidPassword ) {
524            $centralUser->setPassword( null );
525        }
526    }
527
528    /**
529     * @inheritDoc
530     */
531    protected function setForceHTTPSCookie( $set, ?SessionBackend $backend, WebRequest $request ) {
532        // Do nothing. We don't support mixed-protocol HTTP/HTTPS wikis in CentralAuth,
533        // so this cookie is not needed.
534    }
535
536    /** @inheritDoc */
537    protected function setLoggedOutCookie( $loggedOut, WebRequest $request ) {
538        if ( $loggedOut + 86400 > time() &&
539            $loggedOut !== (int)$this->getCookie(
540                $request, 'LoggedOut', $this->centralCookieOptions['prefix'] )
541        ) {
542            $request->response()->setCookie( 'LoggedOut', (string)$loggedOut, $loggedOut + 86400,
543                $this->centralCookieOptions );
544        }
545    }
546
547    /**
548     * @return string[]
549     */
550    public function getVaryCookies() {
551        $cookies = parent::getVaryCookies();
552
553        if ( $this->enable ) {
554            $prefix = $this->centralCookieOptions['prefix'];
555            $cookies[] = $prefix . 'Token';
556            $cookies[] = $prefix . 'LoggedOut';
557            $cookies[] = $this->params['centralSessionName'];
558        }
559
560        return $cookies;
561    }
562
563    /** @inheritDoc */
564    public function suggestLoginUsername( WebRequest $request ) {
565        $name = $this->getCookie( $request, 'User', $this->centralCookieOptions['prefix'] );
566        if ( $name !== null ) {
567            if ( $this->userNameUtils->isTemp( $name ) ) {
568                $name = false;
569            } else {
570                $name = $this->userNameUtils->getCanonical( $name, UserRigorOptions::RIGOR_USABLE );
571            }
572        }
573        return ( $name === false || $name === null )
574            ? parent::suggestLoginUsername( $request )
575            : $name;
576    }
577
578    /**
579     * Fetch the central cookie domain
580     * @return string
581     */
582    public function getCentralCookieDomain() {
583        return $this->centralCookieOptions['domain'];
584    }
585
586    /** @inheritDoc */
587    protected function getExtendedLoginCookies() {
588        $cookies = parent::getExtendedLoginCookies();
589        $cookies[] = 'User';
590        return $cookies;
591    }
592
593    /** @inheritDoc */
594    public function getRememberUserDuration() {
595        // CentralAuth needs User and Token cookies to remember the user. The fallback to
596        // sessions needs UserID as well, so if that one has shorter expiration, the remember
597        // duration will depend on whether the account is attached; let's return the shorter
598        // duration in that case.
599
600        return min(
601            $this->getLoginCookieExpiration( 'User', true ),
602            $this->getLoginCookieExpiration( 'Token', true ),
603            $this->getLoginCookieExpiration( 'UserID', true )
604        ) ?: null;
605    }
606}