Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
CentralAuthTokenSessionProvider
0.00% covered (danger)
0.00%
0 / 96
0.00% covered (danger)
0.00%
0 / 10
1056
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
 makeBogusSessionInfo
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
2
 getTokenFromRequest
n/a
0 / 0
n/a
0 / 0
0
 provideSessionInfo
0.00% covered (danger)
0.00%
0 / 59
0.00% covered (danger)
0.00%
0 / 1
306
 consumeToken
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 persistsSessionId
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 canChangeUser
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 persistSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 unpersistSession
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 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 / 9
0.00% covered (danger)
0.00%
0 / 1
12
1<?php
2
3use MediaWiki\Extension\CentralAuth\CentralAuthSessionManager;
4use MediaWiki\Extension\CentralAuth\CentralAuthUtilityService;
5use MediaWiki\Extension\CentralAuth\User\CentralAuthUser;
6use MediaWiki\Request\WebRequest;
7use MediaWiki\Session\SessionBackend;
8use MediaWiki\Session\SessionInfo;
9use MediaWiki\Session\UserInfo;
10use MediaWiki\User\User;
11use MediaWiki\User\UserIdentityLookup;
12use MediaWiki\User\UserNameUtils;
13use 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 */
24abstract 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}