Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 68 |
|
0.00% |
0 / 3 |
CRAP | |
0.00% |
0 / 1 |
SpecialPageBeforeExecuteHookHandler | |
0.00% |
0 / 68 |
|
0.00% |
0 / 3 |
600 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
onSpecialPageBeforeExecute | |
0.00% |
0 / 57 |
|
0.00% |
0 / 1 |
462 | |||
log | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth\Hooks\Handlers; |
4 | |
5 | use MediaWiki\Auth\AuthManager; |
6 | use MediaWiki\Config\Config; |
7 | use MediaWiki\Extension\CentralAuth\CentralAuthHooks; |
8 | use MediaWiki\Extension\CentralAuth\CentralAuthSessionManager; |
9 | use MediaWiki\Extension\CentralAuth\CentralAuthUtilityService; |
10 | use MediaWiki\Logger\LoggerFactory; |
11 | use MediaWiki\SpecialPage\Hook\SpecialPageBeforeExecuteHook; |
12 | use MediaWiki\SpecialPage\SpecialPage; |
13 | use MediaWiki\Title\Title; |
14 | use MediaWiki\WikiMap\WikiMap; |
15 | use MobileContext; |
16 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
17 | |
18 | /** |
19 | * Triggers a server-side (top-level) central autologin attempt on Special:Userlogin. |
20 | */ |
21 | class SpecialPageBeforeExecuteHookHandler implements SpecialPageBeforeExecuteHook { |
22 | |
23 | /** |
24 | * Name of the cookie that represents that top-level auto-login has been attempted. |
25 | * It is set before the initiation of autologin, cleared on success (or via expiry), |
26 | * and prevents further (or concurrent) attempts. |
27 | * See also ext.centralauth.centralautologin.clearcookie.js. |
28 | */ |
29 | public const AUTOLOGIN_TRIED_COOKIE = 'CentralAuthAnonTopLevel'; |
30 | |
31 | /** |
32 | * Query parameter that marks that we have just returned from a top-level autologin attempt. |
33 | * Used to prevent redirect loops. Normally the cookie would do that, but the client might |
34 | * not record the cookie for some reason. |
35 | */ |
36 | private const AUTOLOGIN_TRIED_QUERY_PARAM = 'centralAuthAutologinTried'; |
37 | |
38 | /** Query parameter used to return error messages from SpecialCentralAutoLogin. */ |
39 | public const AUTOLOGIN_ERROR_QUERY_PARAM = 'centralAuthError'; |
40 | |
41 | /** @var AuthManager */ |
42 | private AuthManager $authManager; |
43 | |
44 | /** @var Config */ |
45 | private Config $config; |
46 | |
47 | /** @var CentralAuthSessionManager */ |
48 | private CentralAuthSessionManager $sessionManager; |
49 | |
50 | /** @var CentralAuthUtilityService */ |
51 | private CentralAuthUtilityService $centralAuthUtilityService; |
52 | |
53 | /** |
54 | * @param AuthManager $authManager |
55 | * @param Config $config |
56 | * @param CentralAuthSessionManager $sessionManager |
57 | * @param CentralAuthUtilityService $centralAuthUtilityService |
58 | */ |
59 | public function __construct( |
60 | AuthManager $authManager, |
61 | Config $config, |
62 | CentralAuthSessionManager $sessionManager, |
63 | CentralAuthUtilityService $centralAuthUtilityService |
64 | ) { |
65 | $this->authManager = $authManager; |
66 | $this->config = $config; |
67 | $this->sessionManager = $sessionManager; |
68 | $this->centralAuthUtilityService = $centralAuthUtilityService; |
69 | } |
70 | |
71 | /** |
72 | * Triggers top-level central autologin attempt on Special:Userlogin, and handles the |
73 | * outcome of such an attempt at the end of the redirect chain. |
74 | * |
75 | * @param SpecialPage $special |
76 | * @param string|null $subPage |
77 | * @return bool |
78 | * |
79 | * @see SpecialCentralAutoLogin |
80 | */ |
81 | public function onSpecialPageBeforeExecute( $special, $subPage ) { |
82 | $request = $special->getRequest(); |
83 | $amKey = 'AuthManagerSpecialPage:return:' . $special->getName(); |
84 | |
85 | // Only attempt top-level autologin if the user is about to log in, the login isn't |
86 | // already in progress, it is a normal login, and there is a central login wiki to use. |
87 | // This check will also pass if the user just finished (successfully or not) the autologin. |
88 | if ( $special->getName() !== 'Userlogin' |
89 | || $request->wasPosted() |
90 | || $subPage |
91 | // Deal with the edge case where AuthManagerSpecialPage::handleReturnBeforeExecute() |
92 | // redirects from Special:Userlogin/return, and the login session is the only thing |
93 | // that gives away that we are in the middle of a remote login flow. |
94 | || $this->authManager->getAuthenticationSessionData( $amKey ) |
95 | // elevated-security reauthentication of already logged-in user |
96 | || $request->getBool( 'force' ) |
97 | || !$this->config->get( 'CentralAuthLoginWiki' ) |
98 | || $this->config->get( 'CentralAuthLoginWiki' ) === WikiMap::getCurrentWikiId() |
99 | ) { |
100 | return true; |
101 | } |
102 | |
103 | $isMobile = CentralAuthHooks::isMobileDomain(); |
104 | |
105 | // Do a top-level autologin if the user needs to log in. Use a cookie to prevent |
106 | // unnecessary autologin attempts if we already know they will fail, and a query parameter |
107 | // (to be set by SpecialCentralAutoLogin) to avoid an infinite loop even if we cannot set |
108 | // cookies for some reason. |
109 | if ( $special->getUser()->isAnon() |
110 | && !$request->getCookie( self::AUTOLOGIN_TRIED_COOKIE, '' ) |
111 | && !$request->getCheck( self::AUTOLOGIN_TRIED_QUERY_PARAM ) |
112 | ) { |
113 | $url = WikiMap::getForeignURL( |
114 | $this->config->get( 'CentralAuthLoginWiki' ), |
115 | 'Special:CentralAutoLogin/checkLoggedIn' |
116 | ); |
117 | if ( $url === false ) { |
118 | // WikiMap misconfigured? |
119 | return true; |
120 | } |
121 | |
122 | $this->log( 'Top-level autologin started', $special, $isMobile ); |
123 | |
124 | $request->response()->setCookie( |
125 | self::AUTOLOGIN_TRIED_COOKIE, |
126 | '1', |
127 | // use 1 day like anon-set.js |
128 | time() + ExpirationAwareness::TTL_DAY, |
129 | // match the behavior of the CentralAuthAnon cookie |
130 | [ 'prefix' => '', 'httpOnly' => false ] |
131 | ); |
132 | |
133 | $returnUrl = wfAppendQuery( $request->getFullRequestURL(), [ |
134 | self::AUTOLOGIN_TRIED_QUERY_PARAM => 1, |
135 | ] ); |
136 | if ( CentralAuthHooks::isMobileDomain() ) { |
137 | // WebRequest::getFullRequestURL() uses $wgServer, not the actual request |
138 | // domain, but we do want to preserve that |
139 | $returnUrl = MobileContext::singleton()->getMobileUrl( $returnUrl ); |
140 | } |
141 | $returnUrlToken = $this->centralAuthUtilityService->tokenize( $returnUrl, |
142 | 'centralautologin-returnurl', $this->sessionManager ); |
143 | $url = wfAppendQuery( $url, [ |
144 | 'type' => 'redirect', |
145 | 'returnUrlToken' => $returnUrlToken, |
146 | 'wikiid' => WikiMap::getCurrentWikiId(), |
147 | 'mobile' => $isMobile ? 1 : null, |
148 | ] ); |
149 | $special->getOutput()->redirect( $url ); |
150 | |
151 | return false; |
152 | } |
153 | |
154 | // Clean up after successful autologin. |
155 | if ( $special->getUser()->isRegistered() |
156 | && $request->getCheck( self::AUTOLOGIN_TRIED_QUERY_PARAM ) |
157 | ) { |
158 | $this->log( 'Top-level autologin succeeded', $special, $isMobile ); |
159 | |
160 | $request->response()->clearCookie( self::AUTOLOGIN_TRIED_COOKIE, [ 'prefix' => '' ] ); |
161 | // If returnto is set, let SpecialUserlogin redirect the user. If it is not set, |
162 | // we would just show the login page, which would be confusing, so send the user away. |
163 | if ( $request->getCheck( 'returnto' ) ) { |
164 | return true; |
165 | } else { |
166 | $special->getOutput()->redirect( Title::newMainPage( $special->getContext() )->getLocalURL() ); |
167 | return false; |
168 | } |
169 | } |
170 | |
171 | // Log failed / prevented autologin. |
172 | if ( $special->getUser()->isAnon() |
173 | && $request->getCheck( self::AUTOLOGIN_TRIED_QUERY_PARAM ) |
174 | ) { |
175 | $error = $request->getRawVal( self::AUTOLOGIN_ERROR_QUERY_PARAM, 'unknown error' ); |
176 | $this->log( "Top-level autologin failed: $error", $special, $isMobile ); |
177 | } elseif ( $special->getUser()->isAnon() |
178 | && $request->getCookie( self::AUTOLOGIN_TRIED_COOKIE, '' ) |
179 | ) { |
180 | $this->log( 'Top-level autologin prevented by cookie', $special, $isMobile ); |
181 | } |
182 | // Else: the user is already logged in and manually visiting the login page, e.g. |
183 | // to log in as another user. Nothing to do. |
184 | |
185 | return true; |
186 | } |
187 | |
188 | private function log( string $message, SpecialPage $special, bool $isMobile ): void { |
189 | $request = $special->getRequest(); |
190 | LoggerFactory::getInstance( 'CentralAuth' )->debug( $message, [ |
191 | 'userAgent' => $request->getHeader( 'User-Agent' ), |
192 | 'isMobile' => $isMobile, |
193 | 'username' => $special->getUser()->isRegistered() ? $special->getUser()->getName() : '', |
194 | 'suggestedLoginUsername' => $request->getSession()->suggestLoginUsername(), |
195 | ] ); |
196 | } |
197 | |
198 | } |