MediaWiki REL1_34
TOTPKey.php
Go to the documentation of this file.
1<?php
2
4
22use Base32\Base32;
23use jakobo\HOTP\HOTP;
26use Psr\Log\LoggerInterface;
28use DomainException;
29use Exception;
30use MWException;
34
42class TOTPKey implements IAuthKey {
47 const MAIN_TOKEN = 1;
48
53 const SCRATCH_TOKEN = -1;
54
56 private $secret;
57
59 private $scratchTokens = [];
60
65 public static function newFromRandom() {
66 $object = new self(
67 Base32::encode( random_bytes( 10 ) ),
68 []
69 );
70
71 $object->regenerateScratchTokens();
72
73 return $object;
74 }
75
82 public static function newFromString( $data ) {
83 $data = json_decode( $data, true );
84 if ( json_last_error() !== JSON_ERROR_NONE ) {
85 return null;
86 }
87 return static::newFromArray( $data );
88 }
89
94 public static function newFromArray( array $data ) {
95 if ( !isset( $data['secret'] ) || !isset( $data['scratch_tokens'] ) ) {
96 return null;
97 }
98 return new static( $data['secret'], $data['scratch_tokens'] );
99 }
100
105 public function __construct( $secret, array $scratchTokens ) {
106 // Currently hardcoded values; might be used in future
107 $this->secret = [
108 'mode' => 'hotp',
109 'secret' => $secret,
110 'period' => 30,
111 'algorithm' => 'SHA1',
112 ];
113 $this->scratchTokens = array_values( $scratchTokens );
114 }
115
119 public function getSecret() {
120 return $this->secret['secret'];
121 }
122
126 public function getScratchTokens() {
128 }
129
136 public function verify( $data, OATHUser $user ) {
137 global $wgOATHAuthWindowRadius;
138
139 $token = $data['token'];
140
141 if ( $this->secret['mode'] !== 'hotp' ) {
142 throw new DomainException( 'OATHAuth extension does not support non-HOTP tokens' );
143 }
144
145 // Prevent replay attacks
146 $store = MediaWikiServices::getInstance()->getMainObjectStash();
147 $uid = CentralIdLookup::factory()->centralIdFromLocalUser( $user->getUser() );
148 $key = $store->makeKey( 'oathauth-totp', 'usedtokens', $uid );
149 $lastWindow = (int)$store->get( $key );
150
151 $retval = false;
152 $results = HOTP::generateByTimeWindow(
153 Base32::decode( $this->secret['secret'] ),
154 $this->secret['period'], -$wgOATHAuthWindowRadius, $wgOATHAuthWindowRadius
155 );
156
157 // Remove any whitespace from the received token, which can be an intended group seperator
158 // or trimmeable whitespace
159 $token = preg_replace( '/\s+/', '', $token );
160
161 $clientIP = $user->getUser()->getRequest()->getIP();
162
163 $logger = $this->getLogger();
164
165 // Check to see if the user's given token is in the list of tokens generated
166 // for the time window.
167 foreach ( $results as $window => $result ) {
168 if ( $window > $lastWindow && $result->toHOTP( 6 ) === $token ) {
169 $lastWindow = $window;
170 $retval = self::MAIN_TOKEN;
171
172 $logger->info( 'OATHAuth user {user} entered a valid OTP from {clientip}', [
173 'user' => $user->getAccount(),
174 'clientip' => $clientIP,
175 ] );
176 break;
177 }
178 }
179
180 // See if the user is using a scratch token
181 if ( !$retval ) {
182 $length = count( $this->scratchTokens );
183 // Detect condition where all scratch tokens have been used
184 if ( $length === 1 && $this->scratchTokens[0] === "" ) {
185 $retval = false;
186 } else {
187 for ( $i = 0; $i < $length; $i++ ) {
188 if ( $token === $this->scratchTokens[$i] ) {
189 // If there is a scratch token, remove it from the scratch token list
190 array_splice( $this->scratchTokens, $i, 1 );
191
192 $logger->info( 'OATHAuth user {user} used a scratch token from {clientip}', [
193 'user' => $user->getAccount(),
194 'clientip' => $clientIP,
195 ] );
196
197 $auth = MediaWikiServices::getInstance()->getService( 'OATHAuth' );
198 $module = $auth->getModuleByKey( 'totp' );
199
201 $userRepo = MediaWikiServices::getInstance()->getService( 'OATHUserRepository' );
202 $user->addKey( $this );
203 $user->setModule( $module );
204 $userRepo->persist( $user, $clientIP );
205 // Only return true if we removed it from the database
206 $retval = self::SCRATCH_TOKEN;
207 break;
208 }
209 }
210 }
211 }
212
213 if ( $retval ) {
214 $store->set(
215 $key,
216 $lastWindow,
217 $this->secret['period'] * ( 1 + 2 * $wgOATHAuthWindowRadius )
218 );
219 } else {
220 $logger->info( 'OATHAuth user {user} failed OTP/scratch token from {clientip}', [
221 'user' => $user->getAccount(),
222 'clientip' => $clientIP,
223 ] );
224
225 // Increase rate limit counter for failed request
226 $user->getUser()->pingLimiter( 'badoath' );
227 }
228
229 return $retval;
230 }
231
232 public function regenerateScratchTokens() {
233 $scratchTokens = [];
234 for ( $i = 0; $i < 10; $i++ ) {
235 $scratchTokens[] = Base32::encode( random_bytes( 10 ) );
236 }
237 $this->scratchTokens = $scratchTokens;
238 }
239
247 public function isScratchToken( $token ) {
248 $token = preg_replace( '/\s+/', '', $token );
249 return in_array( $token, $this->scratchTokens, true );
250 }
251
255 private function getLogger() {
256 return LoggerFactory::getInstance( 'authentication' );
257 }
258
259 public function jsonSerialize() {
260 return [
261 'secret' => $this->getSecret(),
262 'scratch_tokens' => $this->getScratchTokens()
263 ];
264 }
265}
The CentralIdLookup service allows for connecting local users with cluster-wide IDs.
static factory( $providerId=null)
Fetch a CentralIdLookup.
MediaWiki exception.
Class representing a two-factor key.
Definition TOTPKey.php:42
isScratchToken( $token)
Check if a token is one of the scratch tokens for this two factor key.
Definition TOTPKey.php:247
const MAIN_TOKEN
Represents that a token corresponds to the main secret.
Definition TOTPKey.php:47
__construct( $secret, array $scratchTokens)
Definition TOTPKey.php:105
const SCRATCH_TOKEN
Represents that a token corresponds to a scratch token.
Definition TOTPKey.php:53
string[] $scratchTokens
List of scratch tokens.
Definition TOTPKey.php:59
static newFromString( $data)
Create key from json encoded string.
Definition TOTPKey.php:82
array $secret
Two factor binary secret.
Definition TOTPKey.php:56
Class representing a user from OATH's perspective.
Definition OATHUser.php:28
setModule(IModule $module=null)
Sets the module instance associated with this user.
Definition OATHUser.php:149
addKey(IAuthKey $key)
Adds single key to the key array.
Definition OATHUser.php:127
PSR-3 logger instance factory.
MediaWikiServices is the service locator for the application scope of MediaWiki.
static getInstance()
Returns the global default instance of the top level service locator.