Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 84 |
|
0.00% |
0 / 11 |
CRAP | |
0.00% |
0 / 1 |
CentralAuthRedirectingPrimaryAuthenticationProvider | |
0.00% |
0 / 84 |
|
0.00% |
0 / 11 |
992 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
2 | |||
getAuthenticationRequests | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
42 | |||
beginPrimaryAuthentication | |
0.00% |
0 / 34 |
|
0.00% |
0 / 1 |
30 | |||
continuePrimaryAuthentication | |
0.00% |
0 / 31 |
|
0.00% |
0 / 1 |
132 | |||
testUserCanAuthenticate | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
providerNormalizeUsername | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
testUserExists | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
6 | |||
providerAllowsAuthenticationDataChange | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
providerChangeAuthenticationData | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
accountCreationType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
beginPrimaryAccountCreation | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\CentralAuth; |
4 | |
5 | use LogicException; |
6 | use MediaWiki\Auth\AbstractPrimaryAuthenticationProvider; |
7 | use MediaWiki\Auth\AuthenticationRequest; |
8 | use MediaWiki\Auth\AuthenticationResponse; |
9 | use MediaWiki\Auth\AuthManager; |
10 | use MediaWiki\Extension\CentralAuth\Hooks\Handlers\RedirectingLoginHookHandler; |
11 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
12 | use MediaWiki\Logger\LoggerFactory; |
13 | use MediaWiki\MainConfigNames; |
14 | use MediaWiki\User\UserNameUtils; |
15 | use MobileContext; |
16 | use MWCryptRand; |
17 | use RuntimeException; |
18 | use StatusValue; |
19 | use Wikimedia\Rdbms\IDBAccessObject; |
20 | |
21 | /** |
22 | * Redirect-based provider which sends the user to another domain, assumed to be |
23 | * served by the same wiki farm, to log in, and expects to receive the result of |
24 | * that authentication process when the user returns. |
25 | * |
26 | * @see RedirectingLoginHookHandler |
27 | */ |
28 | class CentralAuthRedirectingPrimaryAuthenticationProvider |
29 | extends AbstractPrimaryAuthenticationProvider |
30 | { |
31 | |
32 | public const NON_LOGIN_WIKI_BUTTONREQUEST_NAME = 'nonloginwiki'; |
33 | public const START_TOKEN_KEY_PREFIX = 'centralauth-sul3-start'; |
34 | public const COMPLETE_TOKEN_KEY_PREFIX = 'centralauth-sul3-complete'; |
35 | |
36 | private CentralAuthTokenManager $tokenManager; |
37 | private SharedDomainUtils $sharedDomainUtils; |
38 | private ?MobileContext $mobileContext; |
39 | |
40 | public function __construct( |
41 | CentralAuthTokenManager $tokenManager, |
42 | SharedDomainUtils $sharedDomainUtils, |
43 | ?MobileContext $mobileContext |
44 | ) { |
45 | $this->tokenManager = $tokenManager; |
46 | $this->sharedDomainUtils = $sharedDomainUtils; |
47 | $this->mobileContext = $mobileContext; |
48 | } |
49 | |
50 | /** @inheritDoc */ |
51 | public function getAuthenticationRequests( $action, array $options ) { |
52 | if ( $this->sharedDomainUtils->isSul3Enabled( $this->manager->getRequest() ) |
53 | && !$this->sharedDomainUtils->isSharedDomain() |
54 | ) { |
55 | switch ( $action ) { |
56 | case AuthManager::ACTION_LOGIN: |
57 | case AuthManager::ACTION_CREATE: |
58 | return [ new CentralAuthRedirectingAuthenticationRequest() ]; |
59 | default: |
60 | return []; |
61 | } |
62 | } |
63 | return []; |
64 | } |
65 | |
66 | /** |
67 | * Store a random secret in the session and redirect the user to the central login wiki, |
68 | * passing the secret and the return URL via the token store. The secret will be used on |
69 | * return to prevent a session fixation attack. |
70 | * |
71 | * @inheritDoc |
72 | */ |
73 | public function beginPrimaryAuthentication( array $reqs ) { |
74 | $req = CentralAuthRedirectingAuthenticationRequest::getRequestByName( |
75 | $reqs, |
76 | self::NON_LOGIN_WIKI_BUTTONREQUEST_NAME |
77 | ); |
78 | |
79 | if ( !$req ) { |
80 | return AuthenticationResponse::newAbstain(); |
81 | } |
82 | |
83 | $this->sharedDomainUtils->assertSul3Enabled( $this->manager->getRequest() ); |
84 | $this->sharedDomainUtils->assertIsNotSharedDomain(); |
85 | |
86 | $secret = MWCryptRand::generateHex( 32 ); |
87 | $this->manager->setAuthenticationSessionData( 'CentralAuth:sul3-login:pending', [ |
88 | 'secret' => $secret, |
89 | ] ); |
90 | |
91 | // For the most part, MediaWiki is not aware of mobile domains and uses the standard |
92 | // domain in all URLs it generates; they must be adjusted manually. |
93 | $returnToUrl = $req->returnToUrl; |
94 | if ( $this->mobileContext && $this->mobileContext->usingMobileDomain() ) { |
95 | $returnToUrl = $this->mobileContext->getMobileUrl( $returnToUrl ); |
96 | } |
97 | |
98 | $data = [ |
99 | 'secret' => $secret, |
100 | 'returnUrl' => $returnToUrl, |
101 | ]; |
102 | // ObjectCacheSessionExpiry will limit how long the local login process, which relies |
103 | // on the session, can be finished. Sync the expiry of this token (which will be used |
104 | // when central login ends) with that. |
105 | $expiry = $this->config->get( MainConfigNames::ObjectCacheSessionExpiry ); |
106 | $token = $this->tokenManager->tokenize( |
107 | $data, self::START_TOKEN_KEY_PREFIX, [ 'expiry' => $expiry ] |
108 | ); |
109 | |
110 | if ( $this->manager->getRequest()->getRawVal( 'sul3-action' ) === 'signup' ) { |
111 | $sharedDomainUrl = $this->sharedDomainUtils->getUrlForSharedDomainAction( |
112 | 'signup', |
113 | $this->manager->getRequest() |
114 | ); |
115 | $url = wfAppendQuery( $sharedDomainUrl, [ 'centralauthLoginToken' => $token ] ); |
116 | } else { |
117 | $url = wfAppendQuery( |
118 | $this->sharedDomainUtils->getUrlForSharedDomainAction( 'login' ), |
119 | [ 'centralauthLoginToken' => $token ] |
120 | ); |
121 | } |
122 | |
123 | return AuthenticationResponse::newRedirect( [ new CentralAuthReturnRequest() ], $url ); |
124 | } |
125 | |
126 | /** |
127 | * Verify the secret and log the user in. |
128 | * |
129 | * @inheritDoc |
130 | */ |
131 | public function continuePrimaryAuthentication( array $reqs ) { |
132 | $this->sharedDomainUtils->assertSul3Enabled( $this->manager->getRequest() ); |
133 | $this->sharedDomainUtils->assertIsNotSharedDomain(); |
134 | |
135 | $req = AuthenticationRequest::getRequestByClass( |
136 | $reqs, CentralAuthReturnRequest::class |
137 | ); |
138 | |
139 | if ( !$req ) { |
140 | throw new LogicException( 'CentralAuthReturnRequest not found' ); |
141 | } |
142 | |
143 | $data = $this->tokenManager->detokenizeAndDelete( |
144 | $req->centralauthLoginToken, self::COMPLETE_TOKEN_KEY_PREFIX |
145 | ); |
146 | $sessionData = $this->manager->getAuthenticationSessionData( 'CentralAuth:sul3-login:pending' ); |
147 | if ( $data === false || $sessionData === false ) { |
148 | // TODO this will happen if the user spends too much time on the login form. |
149 | // We should make sure the message is user-friendly. |
150 | return AuthenticationResponse::newFail( wfMessage( 'centralauth-error-badtoken' ) ); |
151 | } |
152 | foreach ( [ 'secret', 'username' ] as $key ) { |
153 | if ( !isset( $data[$key] ) ) { |
154 | throw new LogicException( "$key not found in return data" ); |
155 | } |
156 | } |
157 | if ( !isset( $sessionData['secret'] ) ) { |
158 | throw new LogicException( 'Secret not found in session data' ); |
159 | } |
160 | |
161 | // Only the user who started the authentication process can have the secret in their local |
162 | // session. There is no way to guarantee that the person entering their credentials on the |
163 | // login form on the shared domain is the same; if an attacker initiates a login flow, |
164 | // tricks a victim into visiting the redirect URL returned by beginPrimaryAuthentication(), |
165 | // and then is somehow able to obtain the URL the victim would be redirected back to after |
166 | // submitting the login form, they would get logged in as the victim locally. But there is |
167 | // no way to do that without fundamentally compromising browser, site or network security. |
168 | if ( !$data['secret'] || $data['secret'] !== $sessionData['secret'] ) { |
169 | LoggerFactory::getInstance( 'security' )->error( __CLASS__ . ': Secret mismatch', |
170 | [ |
171 | 'username' => $data['username'], |
172 | ] |
173 | ); |
174 | return AuthenticationResponse::newFail( wfMessage( 'centralauth-error-badtoken' ) ); |
175 | } |
176 | |
177 | $centralUser = CentralAuthUser::getInstanceByName( $data['username'] ); |
178 | if ( $centralUser->getId() !== $data['userId'] ) { |
179 | // Extremely unlikely but technically possible with global rename race conditions |
180 | throw new RuntimeException( 'User ID mismatch' ); |
181 | } |
182 | |
183 | // Refresh the local wiki's "remember me" state based on the |
184 | // corresponding state from the central domain. |
185 | if ( $data['rememberMe'] ) { |
186 | $this->manager->setAuthenticationSessionData( AuthManager::REMEMBER_ME, true ); |
187 | } |
188 | |
189 | return AuthenticationResponse::newPass( $data['username'] ); |
190 | } |
191 | |
192 | /** @inheritDoc */ |
193 | public function testUserCanAuthenticate( $username ) { |
194 | return false; |
195 | } |
196 | |
197 | /** @inheritDoc */ |
198 | public function providerNormalizeUsername( $username ) { |
199 | return null; |
200 | } |
201 | |
202 | /** @inheritDoc */ |
203 | public function testUserExists( $username, $flags = IDBAccessObject::READ_NORMAL ) { |
204 | $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_USABLE ); |
205 | if ( $username === false ) { |
206 | return false; |
207 | } |
208 | |
209 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
210 | return $centralUser->exists(); |
211 | } |
212 | |
213 | /** @inheritDoc */ |
214 | public function providerAllowsAuthenticationDataChange( AuthenticationRequest $req, $checkData = true ) { |
215 | return StatusValue::newGood( 'ignored' ); |
216 | } |
217 | |
218 | /** @inheritDoc */ |
219 | public function providerChangeAuthenticationData( AuthenticationRequest $req ) { |
220 | } |
221 | |
222 | /** @inheritDoc */ |
223 | public function accountCreationType() { |
224 | return self::TYPE_NONE; |
225 | } |
226 | |
227 | /** @inheritDoc */ |
228 | public function beginPrimaryAccountCreation( $user, $creator, array $reqs ) { |
229 | return AuthenticationResponse::newAbstain(); |
230 | } |
231 | } |