Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
50.00% covered (danger)
50.00%
19 / 38
14.29% covered (danger)
14.29%
1 / 7
CRAP
0.00% covered (danger)
0.00%
0 / 1
ElevatedSecurityAuthenticationRequest
50.00% covered (danger)
50.00%
19 / 38
14.29% covered (danger)
14.29%
1 / 7
30.00
0.00% covered (danger)
0.00%
0 / 1
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 create
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
 __sleep
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getFieldInfo
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
1
 loadFromSubmission
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 validate
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 __set_state
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2/**
3 * @license GPL-2.0-or-later
4 * @file
5 */
6
7namespace MediaWiki\Auth;
8
9use LogicException;
10use MediaWiki\Session\Session;
11use StatusValue;
12
13/**
14 * Authentication request for reauthenticating with elevated security
15 *
16 * Represents an elevated-security reauthentication process (ie. the user proving their
17 * identity before getting access to some security-sensitive functionality such as password
18 * change or actions restricted to highly privileged users). This request is managed by
19 * AuthManager and can be initiated by the securityLevel option of getAuthenticationRequests().
20 *
21 * This request being present in the set of requests passed to an authentication provider means
22 * that the user is guaranteed not to change during the authentication process (but the session id
23 * might renew), and after a successful authentication the user will pass a check for
24 * AuthManager::securitySensitiveOperationStatus() with the given security level.
25 * @ingroup Auth
26 * @since 1.47
27 * @see AuthManager::getAuthenticationRequests()
28 * @see AuthManager::securitySensitiveOperationStatus()
29 * @see SpecialPage::checkLoginSecurityLevel()
30 */
31class ElevatedSecurityAuthenticationRequest extends AuthenticationRequest {
32
33    /** Max allowed time between getAuthenticationRequests() and beginAuthentication(). */
34    public const int MAX_AGE = 300;
35
36    /** The user who is being reauthenticated. Should always be an existing local account. */
37    public int $userId;
38
39    /** The security level of the reauthentication process. */
40    public string $securityLevel;
41
42    private string $token;
43
44    private StatusValue $validationStatus;
45
46    /** The user session which will be elevated. */
47    private ?Session $session = null;
48
49    private function __construct() {
50    }
51
52    /**
53     * Create a new elevated-security reauthentication request, and store its data in the
54     * current session.
55     * @param Session $session The user session which will be elevated. Will be persisted.
56     * @param string $securityLevel The security level of the reauthentication process.
57     * @return ElevatedSecurityAuthenticationRequest
58     * @internal for use by AuthManager only
59     */
60    public static function create( Session $session, $securityLevel ) {
61        if ( $session->getUser()->isAnon() ) {
62            throw new LogicException( 'Cannot create an ElevatedSecurityAuthenticationRequest for anon' );
63        }
64
65        $req = new self();
66        $req->userId = $session->getUser()->getId();
67        $req->securityLevel = $securityLevel;
68        $req->session = $session;
69        $req->validationStatus = StatusValue::newFatal( 'authmanager-elevatedsecurity-not-validated' );
70
71        $token = $session->getToken( [ (string)$req->userId, $req->securityLevel ], 'reauth' );
72        $session->persist(); // sanity, in case the caller somehow forgets
73        $req->token = $token->toString();
74
75        return $req;
76    }
77
78    public function __sleep() {
79        // Don't try to serialize the session. Without a session validate() will fail,
80        // but that's OK, this request can only come from getAuthenticationRequests() so
81        // it will only be validated at the beginning of the begin/continue* methods when
82        // it has not gone through session storage yet.
83        return [ 'userId', 'securityLevel', 'token', 'validationStatus' ];
84    }
85
86    /** @inheritDoc */
87    public function getFieldInfo() {
88        return [
89            'elevatedSecurityToken' => [
90                'type' => 'hidden',
91                'value' => $this->token,
92                'label' => wfMessage( 'authmanager-elevatedsecurity-token-label' ),
93                'help' => wfMessage( 'authmanager-elevatedsecurity-token-help' ),
94                'sensitive' => true,
95                'skippable' => true,
96            ],
97        ];
98    }
99
100    /** @inheritDoc */
101    public function loadFromSubmission( array $data ) {
102        $this->validationStatus = StatusValue::newGood();
103
104        $dataToken = $data['elevatedSecurityToken'] ?? null;
105        if ( !$this->session ) {
106            // loadFromSubmission() was called after serialize+unserialize. This should not happen.
107            throw new LogicException( __METHOD__ . ' called on incomplete object' );
108        }
109        $sessionToken = $this->session->getToken( [ (string)$this->userId, $this->securityLevel ], 'reauth' );
110        if ( !$dataToken ) {
111            // Normally we handle missing fields by returning false but ElevatedSecurityAuthRequest
112            // has a special meaning; if it was present in the initial set of requests, it should not
113            // disappear during loading the data. So here we just record what's present, and will
114            // deal with errors in validate().
115            $this->validationStatus->fatal( 'authmanager-elevatedsecurity-missing-token' );
116        } elseif ( !$sessionToken->match( $dataToken, self::MAX_AGE ) ) {
117            $this->validationStatus->fatal( 'authmanager-elevatedsecurity-invalid-token' );
118        }
119
120        return true;
121    }
122
123    /** @inheritDoc */
124    public function validate() {
125        return $this->validationStatus;
126    }
127
128    public static function __set_state( $data ) {
129        $ret = new static();
130        foreach ( $data as $k => $v ) {
131            $ret->$k = $v;
132        }
133        return $ret;
134    }
135
136}