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