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