Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 96 |
|
0.00% |
0 / 10 |
CRAP | |
0.00% |
0 / 1 |
CentralAuthTokenSessionProvider | |
0.00% |
0 / 96 |
|
0.00% |
0 / 10 |
1056 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
2 | |||
makeBogusSessionInfo | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
2 | |||
getTokenFromRequest | n/a |
0 / 0 |
n/a |
0 / 0 |
0 | |||||
provideSessionInfo | |
0.00% |
0 / 59 |
|
0.00% |
0 / 1 |
306 | |||
consumeToken | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
6 | |||
persistsSessionId | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
canChangeUser | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
persistSession | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
unpersistSession | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
invalidateSessionsForUser | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
20 | |||
preventSessionsForUser | |
0.00% |
0 / 9 |
|
0.00% |
0 / 1 |
12 |
1 | <?php |
2 | |
3 | use MediaWiki\Extension\CentralAuth\CentralAuthSessionManager; |
4 | use MediaWiki\Extension\CentralAuth\CentralAuthUtilityService; |
5 | use MediaWiki\Extension\CentralAuth\User\CentralAuthUser; |
6 | use MediaWiki\Request\WebRequest; |
7 | use MediaWiki\Session\SessionBackend; |
8 | use MediaWiki\Session\SessionInfo; |
9 | use MediaWiki\Session\UserInfo; |
10 | use MediaWiki\User\User; |
11 | use MediaWiki\User\UserIdentityLookup; |
12 | use MediaWiki\User\UserNameUtils; |
13 | use Wikimedia\LightweightObjectStore\ExpirationAwareness; |
14 | |
15 | /** |
16 | * Base class for token based CentralAuth SessionProviders for use with different kinds of APIs. |
17 | * |
18 | * This base class provides functionality for looking at a token from the request, |
19 | * and checking that it corresponds to an existing token generated by |
20 | * CentralAuthTokenProvider. |
21 | * If the token is present but invalid, a CentralAuthTokenSessionProvider returns a |
22 | * bogus SessionInfo to prevent other SessionProviders from establishing a session. |
23 | */ |
24 | abstract class CentralAuthTokenSessionProvider extends \MediaWiki\Session\SessionProvider { |
25 | private UserIdentityLookup $userIdentityLookup; |
26 | private CentralAuthSessionManager $sessionManager; |
27 | private CentralAuthUtilityService $utilityService; |
28 | |
29 | /** |
30 | * @param UserIdentityLookup $userIdentityLookup |
31 | * @param CentralAuthSessionManager $sessionManager |
32 | * @param CentralAuthUtilityService $utilityService |
33 | */ |
34 | public function __construct( |
35 | UserIdentityLookup $userIdentityLookup, |
36 | CentralAuthSessionManager $sessionManager, |
37 | CentralAuthUtilityService $utilityService |
38 | ) { |
39 | parent::__construct(); |
40 | $this->userIdentityLookup = $userIdentityLookup; |
41 | $this->sessionManager = $sessionManager; |
42 | $this->utilityService = $utilityService; |
43 | } |
44 | |
45 | /** |
46 | * Returns a bogus session, which can be used to prevent other SessionProviders |
47 | * from attemption to establish a session. |
48 | * |
49 | * May be overridden by subclasses to somehow cause error handling ot be triggered later. |
50 | * |
51 | * Per default, it just returns null. |
52 | * |
53 | * @param string $code Error code |
54 | * @param string|array $error Error message key, or key+parameters |
55 | * @return SessionInfo|null |
56 | */ |
57 | protected function makeBogusSessionInfo( $code, $error ) { |
58 | // Then return an appropriate SessionInfo |
59 | $id = $this->hashToSessionId( 'bogus' ); |
60 | return new SessionInfo( SessionInfo::MAX_PRIORITY, [ |
61 | 'provider' => $this, |
62 | 'id' => $id, |
63 | 'userInfo' => UserInfo::newAnonymous(), |
64 | 'persisted' => false, |
65 | 'metadata' => [ |
66 | 'error-code' => $code, |
67 | 'error' => $error, |
68 | ], |
69 | ] ); |
70 | } |
71 | |
72 | /** |
73 | * @param WebRequest $request |
74 | * |
75 | * @return string|null |
76 | */ |
77 | abstract protected function getTokenFromRequest( WebRequest $request ); |
78 | |
79 | /** @inheritDoc */ |
80 | public function provideSessionInfo( WebRequest $request ) { |
81 | $oneTimeToken = $this->getTokenFromRequest( $request ); |
82 | if ( $oneTimeToken === null ) { |
83 | return null; |
84 | } |
85 | |
86 | $this->logger->debug( __METHOD__ . ': Found a token!' ); |
87 | |
88 | $key = $this->sessionManager->makeTokenKey( 'api-token', $oneTimeToken ); |
89 | $timeout = $this->getConfig()->get( 'CentralAuthTokenSessionTimeout' ); |
90 | |
91 | $data = $this->utilityService->getKeyValueUponExistence( |
92 | $this->sessionManager->getTokenStore(), $key, $timeout |
93 | ); |
94 | |
95 | if ( !is_array( $data ) || |
96 | !isset( $data['userName'] ) || |
97 | !isset( $data['token'] ) || |
98 | !isset( $data['origin'] ) || |
99 | !isset( $data['originSessionId'] ) |
100 | ) { |
101 | $this->logger->info( __METHOD__ . ': centralauthtoken is invalid' ); |
102 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
103 | } |
104 | |
105 | $userName = $data['userName']; |
106 | $authToken = $data['token']; |
107 | |
108 | // Clean up username |
109 | $userName = $this->userNameUtils->getCanonical( $userName, UserNameUtils::RIGOR_VALID ); |
110 | if ( !$userName ) { |
111 | $this->logger->info( __METHOD__ . ': invalid username' ); |
112 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
113 | } |
114 | if ( !$this->userNameUtils->isUsable( $userName ) ) { |
115 | $this->logger->info( __METHOD__ . ': unusable username' ); |
116 | return $this->makeBogusSessionInfo( 'badusername', |
117 | [ 'apierror-centralauth-badusername', wfEscapeWikiText( $userName ) ] ); |
118 | } |
119 | |
120 | // Try the central user |
121 | $centralUser = CentralAuthUser::getInstanceByName( $userName ); |
122 | |
123 | // Skip if they're being renamed |
124 | if ( $centralUser->renameInProgress() ) { |
125 | $this->logger->info( __METHOD__ . ': rename in progress' ); |
126 | return $this->makeBogusSessionInfo( |
127 | 'renameinprogress', 'apierror-centralauth-renameinprogress' |
128 | ); |
129 | } |
130 | |
131 | if ( !$centralUser->exists() ) { |
132 | $this->logger->info( __METHOD__ . ': global account doesn\'t exist' ); |
133 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
134 | } |
135 | if ( !$centralUser->isAttached() ) { |
136 | $userIdentity = $this->userIdentityLookup->getUserIdentityByName( $userName ); |
137 | if ( $userIdentity && $userIdentity->isRegistered() ) { |
138 | $this->logger->info( __METHOD__ . ': not attached and local account exists' ); |
139 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
140 | } |
141 | } |
142 | |
143 | $key = $this->sessionManager->makeSessionKey( 'api-token-blacklist', (string)$centralUser->getId() ); |
144 | $sessionStore = $this->sessionManager->getSessionStore(); |
145 | if ( $sessionStore->get( $key ) ) { |
146 | $this->logger->info( __METHOD__ . ': user is blacklisted' ); |
147 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
148 | } |
149 | |
150 | if ( $centralUser->authenticateWithToken( $authToken ) != 'ok' ) { |
151 | $this->logger->info( __METHOD__ . ': token mismatch' ); |
152 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
153 | } |
154 | |
155 | $this->logger->debug( __METHOD__ . ': logged in from session' ); |
156 | |
157 | $info = [ |
158 | 'userInfo' => UserInfo::newFromName( $userName, true ), |
159 | 'provider' => $this, |
160 | 'id' => $this->hashToSessionId( implode( "\n", $data ) ), |
161 | 'persisted' => true, |
162 | 'forceUse' => true, |
163 | ]; |
164 | |
165 | if ( !$this->consumeToken( $oneTimeToken ) ) { |
166 | // Raced out trying to mark the token as expired |
167 | return $this->makeBogusSessionInfo( 'badtoken', 'apierror-centralauth-badtoken' ); |
168 | } |
169 | |
170 | return new SessionInfo( SessionInfo::MAX_PRIORITY, $info ); |
171 | } |
172 | |
173 | /** |
174 | * @param string $token |
175 | * |
176 | * @return bool |
177 | */ |
178 | protected function consumeToken( $token ) { |
179 | $tokenStore = $this->sessionManager->getTokenStore(); |
180 | $key = $this->sessionManager->makeTokenKey( 'api-token', $token ); |
181 | |
182 | if ( !$tokenStore->changeTTL( $key, time() - 3600 ) ) { |
183 | $this->logger->error( 'Raced out trying to mark the token as expired' ); |
184 | return false; |
185 | } |
186 | |
187 | return true; |
188 | } |
189 | |
190 | public function persistsSessionId() { |
191 | return false; |
192 | } |
193 | |
194 | public function canChangeUser() { |
195 | return false; |
196 | } |
197 | |
198 | /** @inheritDoc */ |
199 | public function persistSession( |
200 | SessionBackend $session, WebRequest $request |
201 | ) { |
202 | // Nothing to do |
203 | } |
204 | |
205 | /** @inheritDoc */ |
206 | public function unpersistSession( WebRequest $request ) { |
207 | // Nothing to do |
208 | } |
209 | |
210 | /** @inheritDoc */ |
211 | public function invalidateSessionsForUser( User $user ) { |
212 | $centralUser = CentralAuthUser::getPrimaryInstance( $user ); |
213 | if ( $centralUser->exists() && ( $centralUser->isAttached() || !$user->isRegistered() ) ) { |
214 | $centralUser->resetAuthToken(); |
215 | } |
216 | } |
217 | |
218 | /** @inheritDoc */ |
219 | public function preventSessionsForUser( $username ) { |
220 | $username = $this->userNameUtils->getCanonical( $username, UserNameUtils::RIGOR_VALID ); |
221 | if ( !$username ) { |
222 | return; |
223 | } |
224 | |
225 | $centralUser = CentralAuthUser::getInstanceByName( $username ); |
226 | if ( !$centralUser->exists() ) { |
227 | return; |
228 | } |
229 | |
230 | // Assume blacklisting for a day will be enough because we assume by |
231 | // then CentralAuth itself will have been instructed to more |
232 | // permanently block the user. |
233 | $sessionStore = $this->sessionManager->getSessionStore(); |
234 | $key = $this->sessionManager->makeSessionKey( 'api-token-blacklist', (string)$centralUser->getId() ); |
235 | $sessionStore->set( $key, true, ExpirationAwareness::TTL_DAY ); |
236 | } |
237 | |
238 | } |