Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
47.37% covered (danger)
47.37%
18 / 38
14.29% covered (danger)
14.29%
2 / 14
CRAP
0.00% covered (danger)
0.00%
0 / 1
TOTP
47.37% covered (danger)
47.37%
18 / 38
14.29% covered (danger)
14.29%
2 / 14
78.32
0.00% covered (danger)
0.00%
0 / 1
 getTOTPKeys
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisplayName
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 newKey
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getSecondaryAuthProvider
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 verify
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
5.01
 isEnabled
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getManageForm
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 getDescriptionMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDisableWarningMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getAddKeyMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getLoginSwitchButtonMessage
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 isSpecial
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2
3namespace MediaWiki\Extension\OATHAuth\Module;
4
5use MediaWiki\Context\IContextSource;
6use MediaWiki\Context\RequestContext;
7use MediaWiki\Extension\OATHAuth\Auth\TOTPSecondaryAuthenticationProvider;
8use MediaWiki\Extension\OATHAuth\HTMLForm\OATHAuthOOUIHTMLForm;
9use MediaWiki\Extension\OATHAuth\HTMLForm\TOTPEnableForm;
10use MediaWiki\Extension\OATHAuth\Key\TOTPKey;
11use MediaWiki\Extension\OATHAuth\OATHAuthModuleRegistry;
12use MediaWiki\Extension\OATHAuth\OATHAuthServices;
13use MediaWiki\Extension\OATHAuth\OATHUser;
14use MediaWiki\Extension\OATHAuth\OATHUserRepository;
15use MediaWiki\Extension\OATHAuth\Special\OATHManage;
16use MediaWiki\Logger\LoggerFactory;
17use MediaWiki\Message\Message;
18use UnexpectedValueException;
19
20class TOTP implements IModule {
21    public const MODULE_NAME = "totp";
22
23    /**
24     * @return TOTPKey[]
25     */
26    public static function getTOTPKeys( OATHUser $user ): array {
27        // @phan-suppress-next-line PhanTypeMismatchReturn
28        return $user->getKeysForModule( self::MODULE_NAME );
29    }
30
31    public function __construct(
32        private readonly OATHUserRepository $userRepository,
33    ) {
34    }
35
36    /** @inheritDoc */
37    public function getName(): string {
38        return self::MODULE_NAME;
39    }
40
41    public function getDisplayName(): Message {
42        return wfMessage( 'oathauth-module-totp-label' );
43    }
44
45    public function newKey( array $data ): TOTPKey {
46        if ( !isset( $data['secret'] ) ) {
47            throw new UnexpectedValueException( 'oathauth-invalid-data-format' );
48        }
49
50        return TOTPKey::newFromArray( $data );
51    }
52
53    public function getSecondaryAuthProvider(): TOTPSecondaryAuthenticationProvider {
54        return new TOTPSecondaryAuthenticationProvider(
55            $this,
56            $this->userRepository,
57            OATHAuthServices::getInstance()->getLogger()
58        );
59    }
60
61    /** @inheritDoc */
62    public function verify( OATHUser $user, array $data ): bool {
63        if ( !isset( $data['token'] ) ) {
64            return false;
65        }
66
67        foreach ( self::getTOTPKeys( $user ) as $key ) {
68            if ( $key->verify( $user, $data ) ) {
69                return true;
70            }
71        }
72
73        // Check recovery codes
74        // TODO: We should deprecate (T408043) logging in on the TOTP form using recovery codes, and eventually
75        // remove this ability (T408044).
76
77        /** @var RecoveryCodes $recoveryCodes */
78        $recoveryCodes = OATHAuthServices::getInstance()->getModuleRegistry()
79            ->getModuleByKey( RecoveryCodes::MODULE_NAME );
80        $validRecoveryCode = $recoveryCodes->verify( $user, [ 'recoverycode' => $data['token'] ?? '' ] );
81        if ( $validRecoveryCode ) {
82            LoggerFactory::getInstance( 'authentication' )->info(
83                // phpcs:ignore
84                "OATHAuth {user} used a recovery code from {clientip} on TOTP form.", [
85                    'user' => $user->getUser()->getName(),
86                    'clientip' => RequestContext::getMain()->getRequest()->getIP()
87                ]
88            );
89            return true;
90        }
91
92        return false;
93    }
94
95    /**
96     * Is this module currently enabled for the given user?
97     */
98    public function isEnabled( OATHUser $user ): bool {
99        return (bool)self::getTOTPKeys( $user );
100    }
101
102    public function getManageForm(
103        string $action,
104        OATHUser $user,
105        OATHUserRepository $repo,
106        IContextSource $context,
107        OATHAuthModuleRegistry $registry
108    ): ?OATHAuthOOUIHTMLForm {
109        if ( $action === OATHManage::ACTION_ENABLE ) {
110            return new TOTPEnableForm( $user, $repo, $this, $context, $registry );
111        }
112        return null;
113    }
114
115    /** @inheritDoc */
116    public function getDescriptionMessage(): Message {
117        return wfMessage( 'oathauth-totp-description' );
118    }
119
120    /** @inheritDoc */
121    public function getDisableWarningMessage(): Message {
122        return wfMessage( 'oathauth-totp-disable-warning' );
123    }
124
125    /** @inheritDoc */
126    public function getAddKeyMessage(): Message {
127        return wfMessage( 'oathauth-totp-add-key' );
128    }
129
130    /** @inheritDoc */
131    public function getLoginSwitchButtonMessage(): Message {
132        return wfMessage( 'oathauth-auth-switch-module-label' );
133    }
134
135    /** @inheritDoc */
136    public function isSpecial(): bool {
137        return false;
138    }
139}