View source
<?php
namespace Drupal\ga_login\Plugin\TfaValidation;
use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\Config;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\encrypt\EncryptionProfileManagerInterface;
use Drupal\encrypt\EncryptServiceInterface;
use Drupal\tfa\Plugin\TfaBasePlugin;
use Drupal\tfa\Plugin\TfaValidationInterface;
use Drupal\user\UserDataInterface;
use Otp\GoogleAuthenticator;
use Otp\Otp;
use ParagonIE\ConstantTime\Encoding;
use Symfony\Component\DependencyInjection\ContainerInterface;
class GALoginTotpValidation extends TfaBasePlugin implements TfaValidationInterface, ContainerFactoryPluginInterface {
use StringTranslationTrait;
public $auth;
protected $timeSkew;
protected $siteNamePrefix;
protected $namePrefix;
protected $issuer;
protected $alreadyAccepted;
protected $time;
public function __construct(array $configuration, $plugin_id, $plugin_definition, UserDataInterface $user_data, EncryptionProfileManagerInterface $encryption_profile_manager, EncryptServiceInterface $encrypt_service, ConfigFactoryInterface $config_factory, TimeInterface $time) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $user_data, $encryption_profile_manager, $encrypt_service);
$this->auth = new \StdClass();
$this->auth->otp = new Otp();
$this->auth->ga = new GoogleAuthenticator();
$plugin_settings = $config_factory
->get('tfa.settings')
->get('validation_plugin_settings');
$settings = isset($plugin_settings['ga_login_totp']) ? $plugin_settings['ga_login_totp'] : [];
$settings = array_replace([
'time_skew' => 2,
'site_name_prefix' => TRUE,
'name_prefix' => 'TFA',
'issuer' => 'Drupal',
], $settings);
$this->timeSkew = $settings['time_skew'];
$this->siteNamePrefix = $settings['site_name_prefix'];
$this->namePrefix = $settings['name_prefix'];
$this->issuer = $settings['issuer'];
$this->alreadyAccepted = FALSE;
$this->time = $time;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static($configuration, $plugin_id, $plugin_definition, $container
->get('user.data'), $container
->get('encrypt.encryption_profile.manager'), $container
->get('encryption'), $container
->get('config.factory'), $container
->get('datetime.time'));
}
public function ready() {
return $this
->getSeed() !== FALSE;
}
public function getForm(array $form, FormStateInterface $form_state) {
$message = $this
->t('Verification code is application generated and @length digits long.', [
'@length' => $this->codeLength,
]);
if ($this
->getUserData('tfa', 'tfa_recovery_code', $this->uid, $this->userData)) {
$message .= '<br/>' . $this
->t("Can't access your account? Use one of your recovery codes.");
}
$form['code'] = [
'#type' => 'textfield',
'#title' => $this
->t('Application verification code'),
'#description' => $message,
'#required' => TRUE,
'#attributes' => [
'autocomplete' => 'off',
'autofocus' => 'autofocus',
],
];
$form['actions']['#type'] = 'actions';
$form['actions']['login'] = [
'#type' => 'submit',
'#button_type' => 'primary',
'#value' => $this
->t('Verify'),
];
return $form;
}
public function buildConfigurationForm(Config $config, array $state = []) {
$settings_form['time_skew'] = [
'#type' => 'textfield',
'#title' => $this
->t('Number of Accepted Codes'),
'#default_value' => $this->timeSkew,
'#description' => $this
->t('Number of past codes to consider valid. Codes are generated every 30 seconds, so setting this value to 10 would allow each code to work for five minutes.'),
'#size' => 2,
'#states' => $state,
'#required' => TRUE,
];
$settings_form['site_name_prefix'] = [
'#type' => 'checkbox',
'#title' => $this
->t('Use site name as OTP QR code name prefix.'),
'#default_value' => $this->siteNamePrefix,
'#description' => $this
->t('If checked, the site name will be used instead of a static string. This can be useful for multi-site installations.'),
'#states' => $state,
];
$state['visible'] += [
':input[name="validation_plugin_settings[ga_login_totp][site_name_prefix]"]' => [
'checked' => FALSE,
],
];
$settings_form['name_prefix'] = [
'#type' => 'textfield',
'#title' => $this
->t('OTP QR Code Prefix'),
'#default_value' => $this->namePrefix ?: 'tfa',
'#description' => $this
->t('Prefix for OTP QR code names. Suffix is account username.'),
'#size' => 15,
'#states' => $state,
];
$settings_form['issuer'] = [
'#type' => 'textfield',
'#title' => $this
->t('Issuer'),
'#default_value' => $this->issuer,
'#description' => $this
->t('The provider or service this account is associated with.'),
'#size' => 15,
'#required' => TRUE,
];
return $settings_form;
}
public function validateForm(array $form, FormStateInterface $form_state) {
$values = $form_state
->getValues();
if (!$this
->validate($values['code'])) {
$this->errorMessages['code'] = $this
->t('Invalid application code. Please try again.');
if ($this->alreadyAccepted) {
$form_state
->clearErrors();
$this->errorMessages['code'] = $this
->t('Invalid code, it was recently used for a login. Please try a new code.');
}
return FALSE;
}
else {
$this
->storeAcceptedCode($values['code']);
return TRUE;
}
}
public function validateRequest($code) {
if ($this
->validate($code)) {
$this
->storeAcceptedCode($code);
return TRUE;
}
return FALSE;
}
protected function validate($code) {
$code = preg_replace('/\\s+/', '', $code);
if ($this
->alreadyAcceptedCode($code)) {
$this->isValid = FALSE;
}
else {
$seed = $this
->getSeed();
$this->isValid = $seed && $this->auth->otp
->checkTotp(Encoding::base32DecodeUpper($seed), $code, $this->timeSkew);
}
return $this->isValid;
}
public function isAlreadyAccepted() {
return $this->alreadyAccepted;
}
protected function getSeed() {
$result = $this
->getUserData('tfa', 'tfa_totp_seed', $this->uid, $this->userData);
if (!empty($result)) {
$encrypted = base64_decode($result['seed']);
$seed = $this
->decrypt($encrypted);
if (!empty($seed)) {
return $seed;
}
}
return FALSE;
}
public function storeSeed($seed) {
$encrypted = $this
->encrypt($seed);
$record = [
'tfa_totp_seed' => [
'seed' => base64_encode($encrypted),
'created' => $this->time
->getRequestTime(),
],
];
$this
->setUserData('tfa', $record, $this->uid, $this->userData);
}
protected function deleteSeed() {
$this
->deleteUserData('tfa', 'tfa_totp_seed', $this->uid, $this->userData);
}
public function getTimeSkew() {
return $this->timeSkew;
}
}