Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
TOTPEnableForm
0.00% covered (danger)
0.00%
0 / 117
0.00% covered (danger)
0.00%
0 / 8
182
0.00% covered (danger)
0.00%
0 / 1
 getHTML
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 onSuccess
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getDescriptors
0.00% covered (danger)
0.00%
0 / 79
0.00% covered (danger)
0.00%
0 / 1
6
 createResourceList
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
6
 getSecretForDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 getScratchTokensForDisplay
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tokenFormatterFunction
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 onSubmit
0.00% covered (danger)
0.00%
0 / 28
0.00% covered (danger)
0.00%
0 / 1
20
1<?php
2
3namespace MediaWiki\Extension\OATHAuth\HTMLForm;
4
5use Endroid\QrCode\Builder\Builder;
6use Endroid\QrCode\Encoding\Encoding;
7use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh;
8use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeNone;
9use Endroid\QrCode\Writer\SvgWriter;
10use MediaWiki\Config\ConfigException;
11use MediaWiki\Extension\OATHAuth\Key\TOTPKey;
12use MediaWiki\Html\Html;
13use MediaWiki\Logger\LoggerFactory;
14use MediaWiki\Status\Status;
15use MWException;
16
17class TOTPEnableForm extends OATHAuthOOUIHTMLForm {
18    /**
19     * @param array|bool|Status|string $submitResult
20     * @return string
21     */
22    public function getHTML( $submitResult ) {
23        $this->getOutput()->addModuleStyles( 'ext.oath.totp.showqrcode.styles' );
24
25        return parent::getHTML( $submitResult );
26    }
27
28    /**
29     * Add content to output when operation was successful
30     */
31    public function onSuccess() {
32        $this->getOutput()->addWikiMsg( 'oathauth-validatedoath' );
33    }
34
35    /**
36     * @return array
37     */
38    protected function getDescriptors() {
39        $keyData = $this->getRequest()->getSessionData( 'oathauth_totp_key' ) ?? [];
40        $key = TOTPKey::newFromArray( $keyData );
41        if ( !$key instanceof TOTPKey ) {
42            $key = TOTPKey::newFromRandom();
43            $this->getRequest()->setSessionData(
44                'oathauth_totp_key',
45                $key->jsonSerialize()
46            );
47        }
48
49        $secret = $key->getSecret();
50        $issuer = $this->oathUser->getIssuer();
51        $account = $this->oathUser->getAccount();
52        $label = "{$issuer}:{$account}";
53        $qrcodeUrl = "otpauth://totp/"
54            . rawurlencode( $label )
55            . "?secret="
56            . rawurlencode( $secret )
57            . "&issuer="
58            . rawurlencode( $issuer );
59
60        $qrCode = Builder::create()
61            ->writer( new SvgWriter() )
62            ->writerOptions( [ SvgWriter::WRITER_OPTION_EXCLUDE_XML_DECLARATION => true ] )
63            ->data( $qrcodeUrl )
64            ->encoding( new Encoding( 'UTF-8' ) )
65            ->errorCorrectionLevel( new ErrorCorrectionLevelHigh() )
66            ->roundBlockSizeMode( new RoundBlockSizeModeNone() )
67            ->size( 256 )
68            ->margin( 0 )
69            ->build();
70
71        // messages used: oathauth-step1, oathauth-step2, oathauth-step3, oathauth-step4
72        return [
73            'app' => [
74                'type' => 'info',
75                'default' => $this->msg( 'oathauth-step1-test' )->parse(),
76                'raw' => true,
77                'section' => 'step1',
78            ],
79            'qrcode' => [
80                'type' => 'info',
81                'default' =>
82                    $this->msg( 'oathauth-step2-qrcode' )->escaped() . '<br/>'
83                    . Html::element( 'img', [
84                        'src' => $qrCode->getDataUri(),
85                        'alt' => $this->msg( 'oathauth-qrcode-alt' ),
86                        'width' => 256,
87                        'height' => 256,
88                    ] ),
89                'raw' => true,
90                'section' => 'step2',
91            ],
92            'manual' => [
93                'type' => 'info',
94                'label-message' => 'oathauth-step2alt',
95                'default' =>
96                    '<strong>' . $this->msg( 'oathauth-secret' )->escaped() . '</strong><br/>'
97                    . '<kbd>' . $this->getSecretForDisplay( $key ) . '</kbd><br/>'
98                    . '<strong>' . $this->msg( 'oathauth-account' )->escaped() . '</strong><br/>'
99                    . htmlspecialchars( $label ) . '<br/><br/>',
100                'raw' => true,
101                'section' => 'step2',
102            ],
103            'scratchtokens' => [
104                'type' => 'info',
105                'default' =>
106                    '<strong>' . $this->msg( 'oathauth-recoverycodes-important' )->escaped() . '</strong><br/>'
107                    . $this->msg( 'oathauth-recoverycodes' )->parse()
108                    . $this->createResourceList( $this->getScratchTokensForDisplay( $key ) ),
109                'raw' => true,
110                'section' => 'step3',
111            ],
112            'token' => [
113                'type' => 'text',
114                'default' => '',
115                'label-message' => 'oathauth-entertoken',
116                'name' => 'token',
117                'section' => 'step4',
118                'dir' => 'ltr',
119                'autocomplete' => 'one-time-code',
120                'spellcheck' => false,
121            ]
122        ];
123    }
124
125    /**
126     * @param array $resources
127     * @return string
128     */
129    private function createResourceList( $resources ) {
130        $resourceList = '';
131        foreach ( $resources as $resource ) {
132            $resourceList .= Html::rawElement( 'li', [], Html::rawElement( 'kbd', [], $resource ) );
133        }
134        return Html::rawElement( 'ul', [], $resourceList );
135    }
136
137    /**
138     * Retrieve the current secret for display purposes
139     *
140     * The characters of the token are split in groups of 4
141     *
142     * @param TOTPKey $key
143     * @return string
144     */
145    protected function getSecretForDisplay( TOTPKey $key ) {
146        return $this->tokenFormatterFunction( $key->getSecret() );
147    }
148
149    /**
150     * Retrieve current recovery codes for display purposes
151     *
152     * The characters of the token are split in groups of 4
153     *
154     * @param TOTPKey $key
155     * @return string[]
156     */
157    protected function getScratchTokensForDisplay( TOTPKey $key ) {
158        return array_map( [ $this, 'tokenFormatterFunction' ], $key->getScratchTokens() );
159    }
160
161    /**
162     * Formats a key or recovery code by creating groups of 4 separated by space characters
163     *
164     * @param string $token Token to format
165     * @return string The token formatted for display
166     */
167    private function tokenFormatterFunction( $token ) {
168        return implode( ' ', str_split( $token, 4 ) );
169    }
170
171    /**
172     * @param array $formData
173     * @return array|bool
174     * @throws ConfigException
175     * @throws MWException
176     */
177    public function onSubmit( array $formData ) {
178        $keyData = $this->getRequest()->getSessionData( 'oathauth_totp_key' ) ?? [];
179        $key = TOTPKey::newFromArray( $keyData );
180        if ( !$key instanceof TOTPKey ) {
181            return [ 'oathauth-invalidrequest' ];
182        }
183
184        if ( $key->isScratchToken( $formData['token'] ) ) {
185            // A recovery code is not allowed for enrollment
186            LoggerFactory::getInstance( 'authentication' )->info(
187                'OATHAuth {user} attempted to enable 2FA using a recovery code from {clientip}', [
188                    'user' => $this->getUser()->getName(),
189                    'clientip' => $this->getRequest()->getIP(),
190                ]
191            );
192            return [ 'oathauth-noscratchforvalidation' ];
193        }
194        if ( !$key->verify( [ 'token' => $formData['token'] ], $this->oathUser ) ) {
195            LoggerFactory::getInstance( 'authentication' )->info(
196                'OATHAuth {user} failed to provide a correct token while enabling 2FA from {clientip}', [
197                    'user' => $this->getUser()->getName(),
198                    'clientip' => $this->getRequest()->getIP(),
199                ]
200            );
201            return [ 'oathauth-failedtovalidateoath' ];
202        }
203
204        $this->getRequest()->setSessionData( 'oathauth_totp_key', null );
205        $this->oathRepo->createKey(
206            $this->oathUser,
207            $this->module,
208            $key->jsonSerialize(),
209            $this->getRequest()->getIP()
210        );
211
212        return true;
213    }
214}