Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
CRAP | |
0.00% |
0 / 1 |
TOTPEnableForm | |
0.00% |
0 / 117 |
|
0.00% |
0 / 8 |
182 | |
0.00% |
0 / 1 |
getHTML | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
onSuccess | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getDescriptors | |
0.00% |
0 / 79 |
|
0.00% |
0 / 1 |
6 | |||
createResourceList | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
6 | |||
getSecretForDisplay | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getScratchTokensForDisplay | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
tokenFormatterFunction | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
onSubmit | |
0.00% |
0 / 28 |
|
0.00% |
0 / 1 |
20 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\OATHAuth\HTMLForm; |
4 | |
5 | use Endroid\QrCode\Builder\Builder; |
6 | use Endroid\QrCode\Encoding\Encoding; |
7 | use Endroid\QrCode\ErrorCorrectionLevel\ErrorCorrectionLevelHigh; |
8 | use Endroid\QrCode\RoundBlockSizeMode\RoundBlockSizeModeNone; |
9 | use Endroid\QrCode\Writer\SvgWriter; |
10 | use MediaWiki\Config\ConfigException; |
11 | use MediaWiki\Extension\OATHAuth\Key\TOTPKey; |
12 | use MediaWiki\Html\Html; |
13 | use MediaWiki\Logger\LoggerFactory; |
14 | use MediaWiki\Status\Status; |
15 | use MWException; |
16 | |
17 | class 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 | } |