diff --git a/.codeclimate.yml b/.codeclimate.yml
new file mode 100644
index 0000000..afed90f
--- /dev/null
+++ b/.codeclimate.yml
@@ -0,0 +1,17 @@
+---
+engines:
+ duplication:
+ enabled: true
+ config:
+ languages:
+ - php
+ fixme:
+ enabled: true
+ phpmd:
+ enabled: true
+ratings:
+ paths:
+ - "**.php"
+exclude_paths:
+- docs/*
+- tests/*
diff --git a/.scrutinizer.yml b/.scrutinizer.yml
index 051ef9a..674c555 100644
--- a/.scrutinizer.yml
+++ b/.scrutinizer.yml
@@ -1,5 +1,4 @@
inherit: true
-
build:
nodes:
analysis:
diff --git a/_config/security.yml b/_config/security.yml
deleted file mode 100644
index 39d08c7..0000000
--- a/_config/security.yml
+++ /dev/null
@@ -1,12 +0,0 @@
----
-Name: totpsecurity
-After:
- - '#coresecurity'
----
-SilverStripe\Core\Injector\Injector:
- SilverStripe\Security\Security:
- properties:
- Authenticators:
- totp: %$ElliotSawyer\TOTPAuthenticator\TOTPAuthenticator
-ElliotSawyer\TOTPAuthenticator\TOTPLoginForm:
- authenticator_class: ElliotSawyer\TOTPAuthenticator\TOTPAuthenticator
diff --git a/composer.json b/composer.json
index 2c8c4f1..77f9e15 100644
--- a/composer.json
+++ b/composer.json
@@ -1,46 +1,50 @@
{
- "name": "elliot-sawyer/totp-authenticator",
- "description": "Enable 2FA authentication with TOTP",
- "type": "silverstripe-vendormodule",
- "license": "BSD-3-Clause",
- "keywords": [
- "silverstripe",
- "2-factor",
- "authentication",
- "module",
- "security"
- ],
- "authors": [
- {
- "name": "Elliot Sawyer"
- }
- ],
- "require": {
- "firesphere/bootstrapmfa": "dev-master#f0fc8eb861b7e5dca3398dd9f59a382f5cdf5a23",
- "endroid/qr-code": "3.2.12",
- "lfkeitel/phptotp": "^1.0",
- "silverstripe/framework": "^4",
- "silverstripe/siteconfig": "^4"
- },
- "require-dev": {
- "phpunit/phpunit": "^5.7",
- "squizlabs/php_codesniffer": "^3.0"
- },
- "autoload": {
- "psr-4": {
- "ElliotSawyer\\TOTPAuthenticator\\": "src/",
- "ElliotSawyer\\TOTPAuthenticator\\Tests\\": "tests/"
- }
- },
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "support": {
- "issues": "https://github.com/elliot-sawyer/totp-authenticator/issues",
- "source": "https://github.com/elliot-sawyer/totp-authenticator"
- },
- "minimum-stability": "dev",
- "prefer-stable": true
+ "name": "elliot-sawyer/totp-authenticator",
+ "description": "Enable 2FA authentication with TOTP",
+ "type": "silverstripe-vendormodule",
+ "license": "BSD-3-Clause",
+ "keywords": [
+ "silverstripe",
+ "2-factor",
+ "authentication",
+ "module",
+ "security",
+ "TOTP",
+ "Authy",
+ "GoogleAuthenticator"
+ ],
+ "authors": [
+ {
+ "name": "Elliot Sawyer"
+ }
+ ],
+ "require": {
+ "firesphere/bootstrapmfa": "dev-extension-hook-before-mfa",
+ "endroid/qr-code": "3.2.12",
+ "spomky-labs/otphp": "^9.1",
+ "silverstripe/framework": "^4"
+ },
+ "require-dev": {
+ "roave/security-advisories": "dev-master",
+ "phpunit/phpunit": "^5.7",
+ "squizlabs/php_codesniffer": "^3.0"
+ },
+ "autoload": {
+ "psr-4": {
+ "ElliotSawyer\\TOTPAuthenticator\\": "src/",
+ "ElliotSawyer\\TOTPAuthenticator\\Tests\\": "tests/"
+ }
+ },
+ "extra": {
+ "installer-name": "totp-authenticator",
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "support": {
+ "issues": "https://github.com/elliot-sawyer/totp-authenticator/issues",
+ "source": "https://github.com/elliot-sawyer/totp-authenticator"
+ },
+ "minimum-stability": "dev",
+ "prefer-stable": true
}
diff --git a/src/Authenticators/TOTPAuthenticator.php b/src/Authenticators/TOTPAuthenticator.php
index df730db..4e67c83 100644
--- a/src/Authenticators/TOTPAuthenticator.php
+++ b/src/Authenticators/TOTPAuthenticator.php
@@ -3,10 +3,13 @@
namespace ElliotSawyer\TOTPAuthenticator;
use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
-use lfkeitel\phptotp\Base32;
-use lfkeitel\phptotp\Totp;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Configurable;
+use Firesphere\BootstrapMFA\Forms\BootstrapMFALoginForm;
+use Firesphere\BootstrapMFA\Handlers\BootstrapMFALoginHandler;
+use Firesphere\BootstrapMFA\Interfaces\MFAAuthenticator;
+use OTPHP\TOTP;
+use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
@@ -14,7 +17,7 @@
* Class TOTPAuthenticator
* @package ElliotSawyer\TOTPAuthenticator
*/
-class TOTPAuthenticator extends BootstrapMFAAuthenticator
+class TOTPAuthenticator extends BootstrapMFAAuthenticator implements MFAAuthenticator
{
use Configurable;
@@ -25,44 +28,54 @@ class TOTPAuthenticator extends BootstrapMFAAuthenticator
* a specific TOTP authenticator
*/
private static $algorithm = 'sha1';
-
-
+
/**
- * @param string $link
- * @return \SilverStripe\Security\MemberAuthenticator\LoginHandler|static
+ * Get configured algorithm for TOTP Authenticator
+ *
+ * Must be one of: "sha1", "sha256", "sha512"
+ * If not specified or invalid, default to "sha1"
+ * @return string
*/
- public function getLoginHandler($link)
+ public static function get_algorithm()
{
- return TOTPLoginHandler::create($link, $this);
+ $algorithm = self::config()->get('algorithm');
+
+ return in_array(strtolower($algorithm), ['sha1', 'sha256', 'sha512'])
+ ? $algorithm
+ : 'sha1';
}
/**
* @param array $data
* @param HTTPRequest $request
+ * @param $token
* @param ValidationResult $result
* @return bool|null|Member
* @throws \Exception
*/
- public function validateTOTP($data, $request, &$result)
+ public function verifyMFA($data, $request, $token, &$result)
{
$memberID = $request->getSession()->get(BootstrapMFAAuthenticator::SESSION_KEY . '.MemberID');
// First, let's see if we know the member
- /** @var Member $member */
+ /** @var Member|null $member */
$member = Member::get()->byID($memberID);
// Continue if we have a valid member
if ($member && $member instanceof Member) {
- if (!isset($data['token'])) {
+ if (!$token) {
$member->registerFailedLogin();
$result->addError(_t(self::class . '.NOTOKEN', 'No token sent'));
} else {
- $secret = Base32::decode($member->TOTPSecret);
- $key = $this->getTokenFromTOTP($secret);
- $user_submitted_key = $data['token'];
+ /** @var TOTPProvider $provider */
+ $provider = Injector::inst()->get(TOTPProvider::class);
+ $provider->setMember($member);
+ /** @var TOTP $totp */
+ $totp = $provider->fetchToken($token);
+
- if ($user_submitted_key !== $key) {
+ if (!$totp->verify($token)) {
$result->addError(_t(self::class . '.TOTPFAILED', 'TOTP Failed'));
}
}
@@ -71,43 +84,26 @@ public function validateTOTP($data, $request, &$result)
if ($result->isValid()) {
return $member;
}
+ } else {
+ $result->addError(_t(self::class . '.NOMEMBER', 'Member not found'));
}
-
- $result->addError(_t(self::class . '.NOMEMBER', 'Member not found'));
-
- return $result;
}
/**
- * Given a TOTP secret, use Totp to resolve to a one time token
- *
- * @param string $secret
- * @param string $algorithm If not provided, will default to the configured algorithm
- * @return bool|int|string
+ * @param BootstrapMFALoginHandler $controller
+ * @param string $name
+ * @return TOTPForm|BootstrapMFALoginForm
*/
- protected function getTokenFromTOTP($secret, $algorithm = '')
+ public function getMFAForm($controller, $name)
{
- if (!$algorithm) {
- $algorithm = self::get_algorithm();
- }
-
- $totp = new Totp($algorithm);
- return $totp->GenerateToken($secret);
+ return TOTPForm::create($controller, $name, $this);
}
/**
- * Get configured algorithm for TOTP Authenticator
- *
- * Must be one of: "sha1", "sha256", "sha512"
- * If not specified or invalid, default to "sha1"
* @return string
*/
- public static function get_algorithm()
+ public function getTokenField()
{
- $algorithm = self::config()->get('algorithm');
-
- return in_array(strtolower($algorithm), ['sha1', 'sha256', 'sha512'])
- ? $algorithm
- : 'sha1';
+ return 'token';
}
}
diff --git a/src/Extensions/MemberExtension.php b/src/Extensions/MemberExtension.php
index 6228d6d..3a7f533 100644
--- a/src/Extensions/MemberExtension.php
+++ b/src/Extensions/MemberExtension.php
@@ -2,21 +2,21 @@
namespace ElliotSawyer\TOTPAuthenticator;
-use Endroid\QrCode\Exception\InvalidWriterException;
use Endroid\QrCode\QrCode;
-use lfkeitel\phptotp\Base32;
-use lfkeitel\phptotp\Totp;
+use OTPHP\TOTP;
+use ParagonIE\ConstantTime\Base32;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\LiteralField;
use SilverStripe\Forms\ToggleCompositeField;
use SilverStripe\ORM\DataExtension;
+use SilverStripe\Security\Member;
use SilverStripe\SiteConfig\SiteConfig;
/**
* Class MemberExtension
*
* @package ElliotSawyer\TOTPAuthenticator
- * @property MemberExtension $owner
+ * @property Member|MemberExtension $owner
* @property string $TOTPSecret
*/
class MemberExtension extends DataExtension
@@ -36,28 +36,22 @@ public function onBeforeWrite()
// Only regenerate if there is no secret and MFA is not enabled yet
// Inherits MFAEnabled from Bootstrap object extension
if (!$this->owner->TOTPSecret || !$this->owner->MFAEnabled) {
- $secret = Totp::GenerateSecret(16);
- $secret = Base32::encode($secret);
+ $secret = Base32::encodeUpper(random_bytes(128)); // We generate our own 1024 bits secret
$this->owner->TOTPSecret = $secret;
}
}
/**
* @param FieldList $fields
- * @throws InvalidWriterException
*/
public function updateCMSFields(FieldList $fields)
{
- if (!$this->owner->exists()) {
- $fields->removeByName('TOTPSecret');
- }
-
- if (strlen($this->owner->TOTPSecret)) {
- $qrcodeURI = $this->GoogleAuthenticatorQRCode();
+ if ($this->owner->TOTPSecret !== '') {
+ $qrcodeURI = $this->getQRCode();
$fields->addFieldToTab('Root.Main', ToggleCompositeField::create(
null,
- _t(self::class . '.CMSTOGGLEQRCODELABEL', 'Second Factor Token Secret'),
- LiteralField::create(null, sprintf("", $qrcodeURI))
+ 'Second Factor Token Secret',
+ LiteralField::create(null, sprintf('', $qrcodeURI))
));
$fields->removeByName('TOTPSecret');
}
@@ -65,33 +59,29 @@ public function updateCMSFields(FieldList $fields)
/**
* @return string
- * @throws InvalidWriterException
*/
- public function GoogleAuthenticatorQRCode()
+ protected function getQRCode()
{
$qrCode = new QrCode($this->generateOTPAuthString());
$qrCode->setSize(300);
$qrCode->setWriterByName('png');
- $qrcodeURI = $qrCode->writeDataUri();
- return $qrcodeURI;
+ return $qrCode->writeDataUri();
}
/**
* @return string
*/
- public function generateOTPAuthString()
+ protected function generateOTPAuthString()
{
- $label = urlencode(SiteConfig::current_site_config()->Title);
+ $issuer = SiteConfig::current_site_config()->Title;
$secret = $this->owner->TOTPSecret;
- $email = $this->owner->Email;
+ $label = $this->owner->Email;
+
+ $totp = TOTP::create($secret);
+ $totp->setIssuer($issuer);
+ $totp->setLabel($label);
- return sprintf(
- 'otpauth://totp/%s:%s?secret=%s&issuer=%s',
- $label,
- $email,
- $secret,
- $label
- );
+ return $totp->getProvisioningUri();
}
}
diff --git a/src/Forms/TOTPForm.php b/src/Forms/TOTPForm.php
index 8e17b51..cac1173 100644
--- a/src/Forms/TOTPForm.php
+++ b/src/Forms/TOTPForm.php
@@ -2,35 +2,38 @@
namespace ElliotSawyer\TOTPAuthenticator;
-use Firesphere\BootstrapMFA\Forms\BootstrapMFALoginForm;
use SilverStripe\Control\RequestHandler;
use SilverStripe\Forms\FieldList;
use SilverStripe\Forms\FormAction;
use SilverStripe\Forms\HiddenField;
use SilverStripe\Forms\PasswordField;
+use SilverStripe\Forms\RequiredFields;
+use SilverStripe\Security\LoginForm;
/**
* Class TOTPForm
* @package ElliotSawyer\TOTPAuthenticator
*/
-class TOTPForm extends BootstrapMFALoginForm
+class TOTPForm extends LoginForm
{
/**
* TOTPForm constructor.
* @param RequestHandler|null $controller
- * @param null $validator
* @param string $name
+ * @param null|TOTPAuthenticator $authenticator
*/
public function __construct(
RequestHandler $controller = null,
- $validator = null,
- $name = self::DEFAULT_NAME
+ $name = self::DEFAULT_NAME,
+ $authenticator = null
) {
$this->controller = $controller;
$fields = $this->getFormFields();
$actions = $this->getFormActions();
+ $validator = RequiredFields::create(['token']);
- parent::__construct($controller, $validator, $name, $fields, $actions);
+ parent::__construct($controller, $name, $fields, $actions, $validator);
+ $this->setAuthenticatorClass(get_class($authenticator));
}
/**
@@ -38,8 +41,10 @@ public function __construct(
*/
public function getFormFields()
{
- $fields = FieldList::create();
- $fields->push(PasswordField::create('token', _t(self::class . '.TOTPCODE', 'TOTP Code')));
+ $fields = FieldList::create([
+ PasswordField::create('token', _t(self::class . '.TOTPCODE', 'TOTP Code')),
+ HiddenField::create('AuthenticationMethod', $this->authenticator_class)
+ ]);
$backURL = $this->controller->getRequest()->getVar('BackURL');
if ($backURL) {
@@ -64,6 +69,9 @@ public function getFormActions()
}
/**
+ * Return the title of the form for use in the frontend
+ * For tabs with multiple login methods, for example.
+ * This replaces the old `get_name` method
* @return string
*/
public function getAuthenticatorName()
diff --git a/src/Handlers/TOTPLoginHandler.php b/src/Handlers/TOTPLoginHandler.php
deleted file mode 100644
index 9ad91ec..0000000
--- a/src/Handlers/TOTPLoginHandler.php
+++ /dev/null
@@ -1,78 +0,0 @@
-get(ValidationResult::class);
- $session = $request->getSession();
-
- $this->request['BackURL'] = !empty($session->get('MFALogin.BackURL')) ? $session->get('MFALogin.BackURL') : '';
- $member = $this->authenticator->validateTOTP($data, $request, $result);
-
- if (!$member instanceof Member) {
- $member = parent::validate($data, $form, $request, $result);
- }
-
- if ($member instanceof Member && $result->isValid()) {
- $member->MFAEnabled = true;
- $member->write();
- $memberData = $session->get('MFALogin');
-
- $this->performLogin($member, $memberData, $request);
- Security::setCurrentUser($member);
- $session->clear('MFAForm');
-
- return $this->redirectAfterSuccessfulLogin();
- }
-
- return $this->redirect($this->link());
- }
-
- /**
- * @return static|TOTPForm
- */
- public function MFAForm()
- {
- return TOTPForm::create(
- $this,
- get_class($this->authenticator),
- 'MFAForm'
- );
- }
-}
diff --git a/src/Providers/TOTPProvider.php b/src/Providers/TOTPProvider.php
index 7a17cf6..2cb6720 100644
--- a/src/Providers/TOTPProvider.php
+++ b/src/Providers/TOTPProvider.php
@@ -3,16 +3,9 @@
namespace ElliotSawyer\TOTPAuthenticator;
+use Firesphere\BootstrapMFA\Interfaces\MFAProvider;
use Firesphere\BootstrapMFA\Providers\BootstrapMFAProvider;
-use Firesphere\BootstrapMFA\Providers\MFAProvider;
-use lfkeitel\phptotp\Base32;
-use lfkeitel\phptotp\Totp;
-use SilverStripe\Core\Injector\Injector;
-use SilverStripe\ORM\ValidationException;
-use SilverStripe\ORM\ValidationResult;
-use SilverStripe\Security\Member;
-use SilverStripe\Security\PasswordEncryptor_NotFoundException;
-use ElliotSawyer\TOTPAuthenticator\TOTPAuthenticator;
+use OTPHP\TOTP;
/**
* Class TOTPProvider
@@ -22,38 +15,18 @@ class TOTPProvider extends BootstrapMFAProvider implements MFAProvider
{
/**
* @param string $token
- * @param null $result
- * @return bool|Member
- * @throws ValidationException
- * @throws PasswordEncryptor_NotFoundException
+ * @return bool|TOTP
* @throws \Exception
*/
- public function verifyToken($token, &$result = null)
+ public function fetchToken($token = null)
{
- if (!$result) {
- $result = Injector::inst()->get(ValidationResult::class);
- }
$member = $this->getMember();
if ($member && $member->ID) {
- if (!$token) {
- $result->addError(_t(self::class . '.INVALIDORMISSINGTOKEN', 'Invalid or missing second factor token'));
- } else {
- $secret = Base32::decode($member->TOTPSecret);
- $algorithm = TOTPAuthenticator::get_algorithm();
+ $algorithm = TOTPAuthenticator::get_algorithm();
- $totp = new Totp($algorithm);
- $key = $totp->GenerateToken($secret);
- $user_submitted_key = $token;
- if ($user_submitted_key !== $key) {
- $result->addError(
- _t(self::class . '.INVALIDORMISSINGTOKEN', 'Invalid or missing second factor token')
- );
- } else {
- return $this->member;
- }
- }
+ return TOTP::create($member->TOTPSecret, 30, $algorithm);
}
- return parent::verifyToken($token, $result);
+ return false;
}
}