3 * Authentication request value object
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.
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.
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
24 namespace MediaWiki\Auth
;
26 use MediaWiki\Language\RawMessage
;
27 use MediaWiki\Message\Message
;
28 use UnexpectedValueException
;
31 * This is a value object for authentication requests.
33 * An AuthenticationRequest represents a set of form fields that are needed on
34 * and provided from a login, account creation, password change or similar form. Form fields can
35 * be shared by multiple AuthenticationRequests (see {@see ::mergeFieldInfo()}).
37 * Authentication providers that expect user input need to implement one or more subclasses
38 * of this class and return them from AuthenticationProvider::getAuthenticationRequests().
39 * A typical subclass would override getFieldInfo() and set $required.
45 abstract class AuthenticationRequest
{
47 /** Indicates that the request is not required for authentication to proceed. */
48 public const OPTIONAL
= 0;
51 * Indicates that the request is required for authentication to proceed.
52 * This will only be used for UI purposes; it is the authentication providers'
53 * responsibility to verify that all required requests are present.
55 public const REQUIRED
= 1;
58 * Indicates that the request is required by a primary authentication
59 * provider. Since the user can choose which primary to authenticate with,
60 * the request might or might not end up being actually required.
62 public const PRIMARY_REQUIRED
= 2;
65 * The AuthManager::ACTION_* constant this request was created to be used for.
66 * Usually set by AuthManager. The *_CONTINUE constants are not used here,
67 * the corresponding "begin" constant is used instead.
71 public $action = null;
74 * Whether the authentication request is required (for login, continue, and link
75 * actions). Setting this to optional is roughly equivalent to setting the 'optional' flag for
76 * all fields in the field info.
78 * Set this to self::OPTIONAL or self::REQUIRED. When coming from a primary provider,
79 * self::REQUIRED will be automatically modified to self::PRIMARY_REQUIRED.
83 public $required = self
::REQUIRED
;
86 * Return-to URL, in case of a REDIRECT AuthenticationResponse. Set by AuthManager.
89 public $returnToUrl = null;
92 * Username. Usually set by AuthManager. See AuthenticationProvider::getAuthenticationRequests()
93 * for details of what this means and how it behaves.
95 * Often this doubles as a normal field (ie. getFieldInfo() has a 'username' key).
99 public $username = null;
102 * Supply a unique key for deduplication
104 * When the AuthenticationRequests instances returned by the providers are
105 * merged, the value returned here is used for keeping only one copy of
106 * duplicate requests.
108 * Subclasses should override this if multiple distinct instances would
109 * make sense, i.e. the request class has internal state of some sort.
111 * This value might be exposed to the user in web forms so it should not
112 * contain private information.
114 * @stable to override
117 public function getUniqueId() {
118 return get_called_class();
122 * Fetch input field info. This will be used in the AuthManager APIs and web UIs to define
123 * API input parameters / form fields and to process the submitted data.
125 * The field info is an associative array mapping field names to info
126 * arrays. The info arrays have the following keys:
127 * - type: (string) Type of input. Types and equivalent HTML widgets are:
128 * - string: <input type="text">
129 * - password: <input type="password">
131 * - checkbox: <input type="checkbox">
132 * - multiselect: More a grid of checkboxes than <select multi>
133 * - button: <input type="submit"> (uses 'label' as button text)
134 * - hidden: Not visible to the user, but needs to be preserved for the next request
135 * - null: No widget, just display the 'label' message.
136 * - options: (array) Maps option values to Messages for the
137 * 'select' and 'multiselect' types.
138 * - value: (string) Value (for 'null' and 'hidden') or default value (for other types).
139 * - label: (Message) Text suitable for a label in an HTML form
140 * - help: (Message) Text suitable as a description of what the field is. Used in API
141 * documentation. To add a help text to the web UI, use the AuthChangeFormFields hook.
142 * - optional: (bool) If set and truthy, the field may be left empty
143 * - sensitive: (bool) If set and truthy, the field is considered sensitive. Code using the
144 * request should avoid exposing the value of the field.
145 * - skippable: (bool) If set and truthy, the client is free to hide this
146 * field from the user to streamline the workflow. If all fields are
147 * skippable (except possibly a single button), no user interaction is
150 * All AuthenticationRequests are populated from the same data, so most of the time you'll
151 * want to prefix fields names with something unique to the extension/provider (although
152 * in some cases sharing the field with other requests is the right thing to do, e.g. for
153 * a 'password' field). When multiple fields have the same name, they will be merged (see
154 * AuthenticationRequests::mergeFieldInfo).
155 * Typically, AuthenticationRequest subclasses define public properties with names matching
156 * the field info keys, and those fields will be populated from the submitted data. More
157 * complex behavior can be implemented by overriding {@see ::loadFromSubmission()}.
159 * @return array As above
160 * @phan-return array<string,array{type:string,options?:array,value?:string,label:Message,help:Message,optional?:bool,sensitive?:bool,skippable?:bool}>
162 abstract public function getFieldInfo();
165 * Returns metadata about this request.
167 * This is mainly for the benefit of API clients which need more detailed render hints
168 * than what's available through getFieldInfo(). Semantics are unspecified and left to the
169 * individual subclasses, but the contents of the array should be primitive types so that they
170 * can be transformed into JSON or similar formats.
172 * @stable to override
173 * @return array A (possibly nested) array with primitive types
175 public function getMetadata() {
180 * Initialize form submitted form data.
182 * The default behavior is to check for each key of self::getFieldInfo()
183 * in the submitted data, and copy the value - after type-appropriate transformations -
184 * to $this->$key. Most subclasses won't need to override this; if you do override it,
185 * make sure to always return false if self::getFieldInfo() returns an empty array.
187 * @stable to override
188 * @param array $data Submitted data as an associative array (keys will correspond
190 * @return bool Whether the request data was successfully loaded
192 public function loadFromSubmission( array $data ) {
193 $fields = array_filter( $this->getFieldInfo(), static function ( $info ) {
194 return $info['type'] !== 'null';
200 foreach ( $fields as $field => $info ) {
201 // Checkboxes and buttons are special. Depending on the method used
202 // to populate $data, they might be unset meaning false or they
203 // might be boolean. Further, image buttons might submit the
204 // coordinates of the click rather than the expected value.
205 if ( $info['type'] === 'checkbox' ||
$info['type'] === 'button' ) {
206 $this->$field = ( isset( $data[$field] ) && $data[$field] !== false )
207 ||
( isset( $data["{$field}_x"] ) && $data["{$field}_x"] !== false );
208 if ( !$this->$field && empty( $info['optional'] ) ) {
214 // Multiselect are too, slightly
215 if ( !isset( $data[$field] ) && $info['type'] === 'multiselect' ) {
219 if ( !isset( $data[$field] ) ) {
222 if ( $data[$field] === '' ||
$data[$field] === [] ) {
223 if ( empty( $info['optional'] ) ) {
227 switch ( $info['type'] ) {
229 if ( !isset( $info['options'][$data[$field]] ) ) {
235 $data[$field] = (array)$data[$field];
236 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset required for multiselect
237 $allowed = array_keys( $info['options'] );
238 if ( array_diff( $data[$field], $allowed ) !== [] ) {
245 $this->$field = $data[$field];
252 * Describe the credentials represented by this request
254 * This is used on requests returned by
255 * AuthenticationProvider::getAuthenticationRequests() for ACTION_LINK
256 * and ACTION_REMOVE and for requests returned in
257 * AuthenticationResponse::$linkRequest to create useful user interfaces.
259 * @stable to override
261 * @return Message[] with the following keys:
262 * - provider: A Message identifying the service that provides
263 * the credentials, e.g. the name of the third party authentication
265 * - account: A Message identifying the credentials themselves,
266 * e.g. the email address used with the third party authentication
269 public function describeCredentials() {
271 'provider' => new RawMessage( '$1', [ get_called_class() ] ),
272 'account' => new RawMessage( '$1', [ $this->getUniqueId() ] ),
277 * Update a set of requests with form submit data, discarding ones that fail
279 * @param AuthenticationRequest[] $reqs
281 * @return AuthenticationRequest[]
283 public static function loadRequestsFromSubmission( array $reqs, array $data ) {
285 foreach ( $reqs as $req ) {
286 if ( $req->loadFromSubmission( $data ) ) {
294 * Select a request by class name.
297 * @param AuthenticationRequest[] $reqs
298 * @param string $class Class name
299 * @phan-param class-string<T> $class
300 * @param bool $allowSubclasses If true, also returns any request that's a subclass of the given
302 * @return AuthenticationRequest|null Returns null if there is not exactly
303 * one matching request.
304 * @phan-return T|null
306 public static function getRequestByClass( array $reqs, $class, $allowSubclasses = false ) {
307 $requests = array_filter( $reqs, static function ( $req ) use ( $class, $allowSubclasses ) {
308 if ( $allowSubclasses ) {
309 return is_a( $req, $class, false );
311 return get_class( $req ) === $class;
314 // @phan-suppress-next-line PhanTypeMismatchReturn False positive
315 return count( $requests ) === 1 ?
reset( $requests ) : null;
319 * Get the username from the set of requests
321 * Only considers requests that have a "username" field.
323 * @param AuthenticationRequest[] $reqs
324 * @return string|null
325 * @throws UnexpectedValueException If multiple different usernames are present.
327 public static function getUsernameFromRequests( array $reqs ) {
330 foreach ( $reqs as $req ) {
331 $info = $req->getFieldInfo();
332 if ( $info && array_key_exists( 'username', $info ) && $req->username
!== null ) {
333 if ( $username === null ) {
334 $username = $req->username
;
335 $otherClass = get_class( $req );
336 } elseif ( $username !== $req->username
) {
337 $requestClass = get_class( $req );
338 throw new UnexpectedValueException( "Conflicting username fields: \"{$req->username}\" from "
339 // @phan-suppress-next-line PhanTypeSuspiciousStringExpression $otherClass always set
340 . "$requestClass::\$username vs. \"$username\" from $otherClass::\$username" );
348 * Merge the output of multiple AuthenticationRequest::getFieldInfo() calls.
349 * @param AuthenticationRequest[] $reqs
350 * @return array Field info in the same format as getFieldInfo().
351 * @throws UnexpectedValueException If the requests include fields with the same name but
352 * incompatible definitions (e.g. different field types).
354 public static function mergeFieldInfo( array $reqs ) {
357 // fields that are required by some primary providers but not others are not actually required
358 $sharedRequiredPrimaryFields = null;
359 foreach ( $reqs as $req ) {
360 if ( $req->required
!== self
::PRIMARY_REQUIRED
) {
364 foreach ( $req->getFieldInfo() as $fieldName => $options ) {
365 if ( empty( $options['optional'] ) ) {
366 $required[] = $fieldName;
369 if ( $sharedRequiredPrimaryFields === null ) {
370 $sharedRequiredPrimaryFields = $required;
372 $sharedRequiredPrimaryFields = array_intersect( $sharedRequiredPrimaryFields, $required );
376 foreach ( $reqs as $req ) {
377 $info = $req->getFieldInfo();
382 foreach ( $info as $name => $options ) {
384 // If the request isn't required, its fields aren't required either.
385 $req->required
=== self
::OPTIONAL
386 // If there is a primary not requiring this field, no matter how many others do,
387 // authentication can proceed without it.
388 ||
( $req->required
=== self
::PRIMARY_REQUIRED
389 // @phan-suppress-next-line PhanTypeMismatchArgumentNullableInternal False positive
390 && !in_array( $name, $sharedRequiredPrimaryFields, true ) )
392 $options['optional'] = true;
394 $options['optional'] = !empty( $options['optional'] );
397 $options['sensitive'] = !empty( $options['sensitive'] );
398 $type = $options['type'];
400 if ( !array_key_exists( $name, $merged ) ) {
401 $merged[$name] = $options;
402 } elseif ( $merged[$name]['type'] !== $type ) {
403 throw new UnexpectedValueException( "Field type conflict for \"$name\", " .
404 "\"{$merged[$name]['type']}\" vs \"$type\""
407 if ( isset( $options['options'] ) ) {
408 if ( isset( $merged[$name]['options'] ) ) {
409 $merged[$name]['options'] +
= $options['options'];
411 // @codeCoverageIgnoreStart
412 $merged[$name]['options'] = $options['options'];
413 // @codeCoverageIgnoreEnd
417 $merged[$name]['optional'] = $merged[$name]['optional'] && $options['optional'];
418 $merged[$name]['sensitive'] = $merged[$name]['sensitive'] ||
$options['sensitive'];
420 // No way to merge 'value', 'image', 'help', or 'label', so just use
421 // the value from the first request.
430 * Implementing this mainly for use from the unit tests.
432 * @return AuthenticationRequest
434 public static function __set_state( $data ) {
435 // @phan-suppress-next-line PhanTypeInstantiateAbstractStatic
437 foreach ( $data as $k => $v ) {