Merge "Update searchdisabled message with docs and better link target"
[mediawiki.git] / includes / Rest / TokenAwareHandlerTrait.php
blob2bf13a9ff5898a43aa4d8a245d70ec0354d1a3f4
1 <?php
3 namespace MediaWiki\Rest;
5 use LogicException;
6 use MediaWiki\Session\Session;
7 use MediaWiki\User\LoggedOutEditToken;
8 use Wikimedia\Message\DataMessageValue;
9 use Wikimedia\Message\MessageValue;
10 use Wikimedia\ParamValidator\ParamValidator;
12 /**
13 * This trait can be used on handlers that choose to support token-based CSRF protection. Note that doing so is
14 * discouraged, and you should preferably require that the endpoint be used with a session provider that is
15 * safe against CSRF, such as OAuth.
16 * @see Handler::requireSafeAgainstCsrf()
18 * @package MediaWiki\Rest
20 trait TokenAwareHandlerTrait {
21 abstract public function getValidatedBody();
23 abstract public function getSession(): Session;
25 /**
26 * Returns the definition for the token parameter, to be used in getBodyValidator().
28 * @return array[]
30 protected function getTokenParamDefinition(): array {
31 return [
32 'token' => [
33 Handler::PARAM_SOURCE => 'body',
34 ParamValidator::PARAM_TYPE => 'string',
35 ParamValidator::PARAM_REQUIRED => false,
36 ParamValidator::PARAM_DEFAULT => '',
41 /**
42 * Determines the CSRF token to be used, possibly taking it from a request parameter.
44 * Returns an empty string if the request isn't known to be safe and
45 * no token was supplied by the client.
46 * Returns null if the session provider is safe against CSRF (and thus no token
47 * is needed)
49 * @return string|null
51 protected function getToken(): ?string {
52 if ( !$this instanceof Handler ) {
53 throw new LogicException( 'This trait must be used on handler classes.' );
56 if ( !$this->needsToken() ) {
57 return null;
60 $body = $this->getValidatedBody();
61 return $body['token'] ?? '';
64 /**
65 * Determines whether a CSRF token is needed.
67 * Returns false if the request has been authenticated in a way that
68 * protects against CSRF, such as OAuth.
70 protected function needsToken(): bool {
71 return !$this->getSession()->getProvider()->safeAgainstCsrf();
74 /**
75 * Returns a standard error message to use when the given CSRF token is invalid.
76 * In the future, this trait may also provide a method for checking the token.
78 protected function getBadTokenMessage(): MessageValue {
79 return DataMessageValue::new( 'rest-badtoken' );
82 /**
83 * Checks that the given CSRF token is valid (or the used authentication method does
84 * not require CSRF).
85 * Note that this method only supports the 'csrf' token type. The body validator must
86 * return an array and include the 'token' field (see getTokenParamDefinition()).
87 * @param bool $allowAnonymousToken Allow anonymous users to pass the check by submitting
88 * an empty token. (This matches how e.g. anonymous editing works on the action API and web.)
89 * @return void
90 * @throws LocalizedHttpException
92 protected function validateToken( bool $allowAnonymousToken = false ): void {
93 if ( $this->getSession()->getProvider()->safeAgainstCsrf() ) {
94 return;
97 $submittedToken = $this->getToken();
98 $sessionToken = null;
99 $isAnon = $this->getSession()->getUser()->isAnon();
100 if ( $allowAnonymousToken && $isAnon ) {
101 $sessionToken = new LoggedOutEditToken();
102 } elseif ( $this->getSession()->hasToken() ) {
103 $sessionToken = $this->getSession()->getToken();
106 if ( $sessionToken && $sessionToken->match( $submittedToken ) ) {
107 return;
108 } elseif ( !$submittedToken ) {
109 throw $this->getBadTokenException( 'rest-badtoken-missing' );
110 } elseif ( $isAnon && !$this->getSession()->isPersistent() ) {
111 // The client probably forgot to authenticate.
112 throw $this->getBadTokenException( 'rest-badtoken-nosession' );
113 } else {
114 // The user submitted a token, the session had a token, but they didn't match.
115 throw new LocalizedHttpException( $this->getBadTokenMessage(), 403 );
120 * @param string $messageKey
121 * @return LocalizedHttpException
122 * @internal For use by the trait only
124 private function getBadTokenException( string $messageKey ): LocalizedHttpException {
125 return new LocalizedHttpException( DataMessageValue::new( $messageKey, [], 'rest-badtoken' ), 403 );