ApiAuthManagerHelper.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. <?php
  2. /**
  3. * Copyright © 2016 Wikimedia Foundation and contributors
  4. *
  5. * This program is free software; you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation; either version 2 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License along
  16. * with this program; if not, write to the Free Software Foundation, Inc.,
  17. * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
  18. * http://www.gnu.org/copyleft/gpl.html
  19. *
  20. * @file
  21. * @since 1.27
  22. */
  23. use MediaWiki\Auth\AuthManager;
  24. use MediaWiki\Auth\AuthenticationRequest;
  25. use MediaWiki\Auth\AuthenticationResponse;
  26. use MediaWiki\Auth\CreateFromLoginAuthenticationRequest;
  27. use MediaWiki\Logger\LoggerFactory;
  28. /**
  29. * Helper class for AuthManager-using API modules. Intended for use via
  30. * composition.
  31. *
  32. * @ingroup API
  33. */
  34. class ApiAuthManagerHelper {
  35. /** @var ApiBase API module, for context and parameters */
  36. private $module;
  37. /** @var string Message output format */
  38. private $messageFormat;
  39. /**
  40. * @param ApiBase $module API module, for context and parameters
  41. */
  42. public function __construct( ApiBase $module ) {
  43. $this->module = $module;
  44. $params = $module->extractRequestParams();
  45. $this->messageFormat = $params['messageformat'] ?? 'wikitext';
  46. }
  47. /**
  48. * Static version of the constructor, for chaining
  49. * @param ApiBase $module API module, for context and parameters
  50. * @return ApiAuthManagerHelper
  51. */
  52. public static function newForModule( ApiBase $module ) {
  53. return new self( $module );
  54. }
  55. /**
  56. * Format a message for output
  57. * @param array &$res Result array
  58. * @param string $key Result key
  59. * @param Message $message
  60. */
  61. private function formatMessage( array &$res, $key, Message $message ) {
  62. switch ( $this->messageFormat ) {
  63. case 'none':
  64. break;
  65. case 'wikitext':
  66. $res[$key] = $message->setContext( $this->module )->text();
  67. break;
  68. case 'html':
  69. $res[$key] = $message->setContext( $this->module )->parseAsBlock();
  70. $res[$key] = Parser::stripOuterParagraph( $res[$key] );
  71. break;
  72. case 'raw':
  73. $res[$key] = [
  74. 'key' => $message->getKey(),
  75. 'params' => $message->getParams(),
  76. ];
  77. ApiResult::setIndexedTagName( $res[$key]['params'], 'param' );
  78. break;
  79. }
  80. }
  81. /**
  82. * Call $manager->securitySensitiveOperationStatus()
  83. * @param string $operation Operation being checked.
  84. * @throws ApiUsageException
  85. */
  86. public function securitySensitiveOperation( $operation ) {
  87. $status = AuthManager::singleton()->securitySensitiveOperationStatus( $operation );
  88. switch ( $status ) {
  89. case AuthManager::SEC_OK:
  90. return;
  91. case AuthManager::SEC_REAUTH:
  92. $this->module->dieWithError( 'apierror-reauthenticate' );
  93. case AuthManager::SEC_FAIL:
  94. $this->module->dieWithError( 'apierror-cannotreauthenticate' );
  95. default:
  96. throw new UnexpectedValueException( "Unknown status \"$status\"" );
  97. }
  98. }
  99. /**
  100. * Filter out authentication requests by class name
  101. * @param AuthenticationRequest[] $reqs Requests to filter
  102. * @param string[] $blacklist Class names to remove
  103. * @return AuthenticationRequest[]
  104. */
  105. public static function blacklistAuthenticationRequests( array $reqs, array $blacklist ) {
  106. if ( $blacklist ) {
  107. $blacklist = array_flip( $blacklist );
  108. $reqs = array_filter( $reqs, function ( $req ) use ( $blacklist ) {
  109. return !isset( $blacklist[get_class( $req )] );
  110. } );
  111. }
  112. return $reqs;
  113. }
  114. /**
  115. * Fetch and load the AuthenticationRequests for an action
  116. * @param string $action One of the AuthManager::ACTION_* constants
  117. * @return AuthenticationRequest[]
  118. */
  119. public function loadAuthenticationRequests( $action ) {
  120. $params = $this->module->extractRequestParams();
  121. $manager = AuthManager::singleton();
  122. $reqs = $manager->getAuthenticationRequests( $action, $this->module->getUser() );
  123. // Filter requests, if requested to do so
  124. $wantedRequests = null;
  125. if ( isset( $params['requests'] ) ) {
  126. $wantedRequests = array_flip( $params['requests'] );
  127. } elseif ( isset( $params['request'] ) ) {
  128. $wantedRequests = [ $params['request'] => true ];
  129. }
  130. if ( $wantedRequests !== null ) {
  131. $reqs = array_filter( $reqs, function ( $req ) use ( $wantedRequests ) {
  132. return isset( $wantedRequests[$req->getUniqueId()] );
  133. } );
  134. }
  135. // Collect the fields for all the requests
  136. $fields = [];
  137. $sensitive = [];
  138. foreach ( $reqs as $req ) {
  139. $info = (array)$req->getFieldInfo();
  140. $fields += $info;
  141. $sensitive += array_filter( $info, function ( $opts ) {
  142. return !empty( $opts['sensitive'] );
  143. } );
  144. }
  145. // Extract the request data for the fields and mark those request
  146. // parameters as used
  147. $data = array_intersect_key( $this->module->getRequest()->getValues(), $fields );
  148. $this->module->getMain()->markParamsUsed( array_keys( $data ) );
  149. if ( $sensitive ) {
  150. $this->module->getMain()->markParamsSensitive( array_keys( $sensitive ) );
  151. $this->module->requirePostedParameters( array_keys( $sensitive ), 'noprefix' );
  152. }
  153. return AuthenticationRequest::loadRequestsFromSubmission( $reqs, $data );
  154. }
  155. /**
  156. * Format an AuthenticationResponse for return
  157. * @param AuthenticationResponse $res
  158. * @return array
  159. */
  160. public function formatAuthenticationResponse( AuthenticationResponse $res ) {
  161. $ret = [
  162. 'status' => $res->status,
  163. ];
  164. if ( $res->status === AuthenticationResponse::PASS && $res->username !== null ) {
  165. $ret['username'] = $res->username;
  166. }
  167. if ( $res->status === AuthenticationResponse::REDIRECT ) {
  168. $ret['redirecttarget'] = $res->redirectTarget;
  169. if ( $res->redirectApiData !== null ) {
  170. $ret['redirectdata'] = $res->redirectApiData;
  171. }
  172. }
  173. if ( $res->status === AuthenticationResponse::REDIRECT ||
  174. $res->status === AuthenticationResponse::UI ||
  175. $res->status === AuthenticationResponse::RESTART
  176. ) {
  177. $ret += $this->formatRequests( $res->neededRequests );
  178. }
  179. if ( $res->status === AuthenticationResponse::FAIL ||
  180. $res->status === AuthenticationResponse::UI ||
  181. $res->status === AuthenticationResponse::RESTART
  182. ) {
  183. $this->formatMessage( $ret, 'message', $res->message );
  184. $ret['messagecode'] = ApiMessage::create( $res->message )->getApiCode();
  185. }
  186. if ( $res->status === AuthenticationResponse::FAIL ||
  187. $res->status === AuthenticationResponse::RESTART
  188. ) {
  189. $this->module->getRequest()->getSession()->set(
  190. 'ApiAuthManagerHelper::createRequest',
  191. $res->createRequest
  192. );
  193. $ret['canpreservestate'] = $res->createRequest !== null;
  194. } else {
  195. $this->module->getRequest()->getSession()->remove( 'ApiAuthManagerHelper::createRequest' );
  196. }
  197. return $ret;
  198. }
  199. /**
  200. * Logs successful or failed authentication.
  201. * @param string $event Event type (e.g. 'accountcreation')
  202. * @param string|AuthenticationResponse $result Response or error message
  203. */
  204. public function logAuthenticationResult( $event, $result ) {
  205. if ( is_string( $result ) ) {
  206. $status = Status::newFatal( $result );
  207. } elseif ( $result->status === AuthenticationResponse::PASS ) {
  208. $status = Status::newGood();
  209. } elseif ( $result->status === AuthenticationResponse::FAIL ) {
  210. $status = Status::newFatal( $result->message );
  211. } else {
  212. return;
  213. }
  214. $module = $this->module->getModuleName();
  215. LoggerFactory::getInstance( 'authevents' )->info( "$module API attempt", [
  216. 'event' => $event,
  217. 'status' => $status,
  218. 'module' => $module,
  219. ] );
  220. }
  221. /**
  222. * Fetch the preserved CreateFromLoginAuthenticationRequest, if any
  223. * @return CreateFromLoginAuthenticationRequest|null
  224. */
  225. public function getPreservedRequest() {
  226. $ret = $this->module->getRequest()->getSession()->get( 'ApiAuthManagerHelper::createRequest' );
  227. return $ret instanceof CreateFromLoginAuthenticationRequest ? $ret : null;
  228. }
  229. /**
  230. * Format an array of AuthenticationRequests for return
  231. * @param AuthenticationRequest[] $reqs
  232. * @return array Will have a 'requests' key, and also 'fields' if $module's
  233. * params include 'mergerequestfields'.
  234. */
  235. public function formatRequests( array $reqs ) {
  236. $params = $this->module->extractRequestParams();
  237. $mergeFields = !empty( $params['mergerequestfields'] );
  238. $ret = [ 'requests' => [] ];
  239. foreach ( $reqs as $req ) {
  240. $describe = $req->describeCredentials();
  241. $reqInfo = [
  242. 'id' => $req->getUniqueId(),
  243. 'metadata' => $req->getMetadata() + [ ApiResult::META_TYPE => 'assoc' ],
  244. ];
  245. switch ( $req->required ) {
  246. case AuthenticationRequest::OPTIONAL:
  247. $reqInfo['required'] = 'optional';
  248. break;
  249. case AuthenticationRequest::REQUIRED:
  250. $reqInfo['required'] = 'required';
  251. break;
  252. case AuthenticationRequest::PRIMARY_REQUIRED:
  253. $reqInfo['required'] = 'primary-required';
  254. break;
  255. }
  256. $this->formatMessage( $reqInfo, 'provider', $describe['provider'] );
  257. $this->formatMessage( $reqInfo, 'account', $describe['account'] );
  258. if ( !$mergeFields ) {
  259. $reqInfo['fields'] = $this->formatFields( (array)$req->getFieldInfo() );
  260. }
  261. $ret['requests'][] = $reqInfo;
  262. }
  263. if ( $mergeFields ) {
  264. $fields = AuthenticationRequest::mergeFieldInfo( $reqs );
  265. $ret['fields'] = $this->formatFields( $fields );
  266. }
  267. return $ret;
  268. }
  269. /**
  270. * Clean up a field array for output
  271. * @param array $fields
  272. * @codingStandardsIgnoreStart
  273. * @phan-param array{type:string,options:array,value:string,label:Message,help:Message,optional:bool,sensitive:bool,skippable:bool} $fields
  274. * @codingStandardsIgnoreEnd
  275. * @return array
  276. */
  277. private function formatFields( array $fields ) {
  278. static $copy = [
  279. 'type' => true,
  280. 'value' => true,
  281. ];
  282. $module = $this->module;
  283. $retFields = [];
  284. foreach ( $fields as $name => $field ) {
  285. $ret = array_intersect_key( $field, $copy );
  286. if ( isset( $field['options'] ) ) {
  287. $ret['options'] = array_map( function ( $msg ) use ( $module ) {
  288. return $msg->setContext( $module )->plain();
  289. }, $field['options'] );
  290. ApiResult::setArrayType( $ret['options'], 'assoc' );
  291. }
  292. $this->formatMessage( $ret, 'label', $field['label'] );
  293. $this->formatMessage( $ret, 'help', $field['help'] );
  294. $ret['optional'] = !empty( $field['optional'] );
  295. $ret['sensitive'] = !empty( $field['sensitive'] );
  296. $retFields[$name] = $ret;
  297. }
  298. ApiResult::setArrayType( $retFields, 'assoc' );
  299. return $retFields;
  300. }
  301. /**
  302. * Fetch the standard parameters this helper recognizes
  303. * @param string $action AuthManager action
  304. * @param string ...$wantedParams Parameters to use
  305. * @return array
  306. */
  307. public static function getStandardParams( $action, ...$wantedParams ) {
  308. $params = [
  309. 'requests' => [
  310. ApiBase::PARAM_TYPE => 'string',
  311. ApiBase::PARAM_ISMULTI => true,
  312. ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-requests', $action ],
  313. ],
  314. 'request' => [
  315. ApiBase::PARAM_TYPE => 'string',
  316. ApiBase::PARAM_REQUIRED => true,
  317. ApiBase::PARAM_HELP_MSG => [ 'api-help-authmanagerhelper-request', $action ],
  318. ],
  319. 'messageformat' => [
  320. ApiBase::PARAM_DFLT => 'wikitext',
  321. ApiBase::PARAM_TYPE => [ 'html', 'wikitext', 'raw', 'none' ],
  322. ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-messageformat',
  323. ],
  324. 'mergerequestfields' => [
  325. ApiBase::PARAM_DFLT => false,
  326. ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-mergerequestfields',
  327. ],
  328. 'preservestate' => [
  329. ApiBase::PARAM_DFLT => false,
  330. ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-preservestate',
  331. ],
  332. 'returnurl' => [
  333. ApiBase::PARAM_TYPE => 'string',
  334. ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-returnurl',
  335. ],
  336. 'continue' => [
  337. ApiBase::PARAM_DFLT => false,
  338. ApiBase::PARAM_HELP_MSG => 'api-help-authmanagerhelper-continue',
  339. ],
  340. ];
  341. $ret = [];
  342. foreach ( $wantedParams as $name ) {
  343. if ( isset( $params[$name] ) ) {
  344. $ret[$name] = $params[$name];
  345. }
  346. }
  347. return $ret;
  348. }
  349. }