Skip to content

Commit

Permalink
Initial rewrite for changes in the Bootstrapping module
Browse files Browse the repository at this point in the history
  • Loading branch information
Firesphere committed Nov 4, 2018
1 parent 2474bda commit 0db94f8
Show file tree
Hide file tree
Showing 10 changed files with 122 additions and 191 deletions.
17 changes: 17 additions & 0 deletions .codeclimate.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
engines:
duplication:
enabled: true
config:
languages:
- php
fixme:
enabled: true
phpmd:
enabled: true
ratings:
paths:
- "**.php"
exclude_paths:
- docs/*
- tests/*
3 changes: 3 additions & 0 deletions .codecov.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
comment: true
codecov:
branch: master
11 changes: 11 additions & 0 deletions .scrutinizer.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
checks:
php: true

build:
nodes:
analysis:
tests:
override: [php-scrutinizer-run]

filter:
paths: ["src/*", "tests/*"]
12 changes: 0 additions & 12 deletions _config/security.yml

This file was deleted.

10 changes: 7 additions & 3 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,20 +8,24 @@
"2-factor",
"authentication",
"module",
"security"
"security",
"TOTP",
"Authy",
"GoogleAuthenticator"
],
"authors": [
{
"name": "Elliot Sawyer"
}
],
"require": {
"firesphere/bootstrapmfa": "dev-master#f0fc8eb861b7e5dca3398dd9f59a382f5cdf5a23",
"firesphere/bootstrapmfa": "dev-extension-hook-before-mfa",
"endroid/qr-code": "3.2.12",
"lfkeitel/phptotp": "^1.0",
"spomky-labs/otphp": "^9.1",
"silverstripe/framework": "^4"
},
"require-dev": {
"roave/security-advisories": "dev-master",
"phpunit/phpunit": "^5.7"
},
"extra": {
Expand Down
75 changes: 42 additions & 33 deletions src/Authenticators/TOTPAuthenticator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,21 @@
namespace ElliotSawyer\TOTPAuthenticator;

use Firesphere\BootstrapMFA\Authenticators\BootstrapMFAAuthenticator;
use Firesphere\BootstrapMFA\Handlers\MFALoginHandler;
use lfkeitel\phptotp\Base32;
use lfkeitel\phptotp\Totp;
use Firesphere\BootstrapMFA\Forms\BootstrapMFALoginForm;
use Firesphere\BootstrapMFA\Handlers\BootstrapMFALoginHandler;
use Firesphere\BootstrapMFA\Interfaces\MFAAuthenticator;
use OTPHP\TOTP;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\Core\Config\Configurable;
use SilverStripe\Core\Injector\Injector;
use SilverStripe\ORM\ValidationResult;
use SilverStripe\Security\Member;
use SilverStripe\Core\Config\Configurable;

/**
* Class TOTPAuthenticator
* @package ElliotSawyer\TOTPAuthenticator
*/
class TOTPAuthenticator extends BootstrapMFAAuthenticator
class TOTPAuthenticator extends BootstrapMFAAuthenticator implements MFAAuthenticator
{
use Configurable;

Expand All @@ -26,47 +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 $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);
$algorithm = self::get_algorithm();
$totp = new Totp($algorithm);
$key = $totp->GenerateToken($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'));
}
}
Expand All @@ -75,26 +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;
/**
* @param BootstrapMFALoginHandler $controller
* @param string $name
* @return TOTPForm|BootstrapMFALoginForm
*/
public function getMFAForm($controller, $name)
{
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';
}
}
42 changes: 18 additions & 24 deletions src/Extensions/MemberExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 TOTPSecondFactorAuthExtension
*
* @package ElliotSawyer\TOTPAuthenticator
* @property MemberExtension $owner
* @property Member|MemberExtension $owner
* @property string $TOTPSecret
*/
class MemberExtension extends DataExtension
Expand All @@ -36,58 +36,52 @@ 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 (strlen($this->owner->TOTPSecret)) {
$qrcodeURI = $this->GoogleAuthenticatorQRCode();
if ($this->owner->TOTPSecret !== '') {
$qrcodeURI = $this->getQRCode();
$fields->addFieldToTab('Root.Main', ToggleCompositeField::create(
null,
'Second Factor Token Secret',
LiteralField::create(null, sprintf("<img src=\"%s\" />", $qrcodeURI))
LiteralField::create(null, sprintf('<img src="%s" />', $qrcodeURI))
));
$fields->removeByName('TOTPSecret');
}
}

/**
* @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;

return sprintf(
'otpauth://totp/%s:%s?secret=%s&issuer=%s',
$label,
$email,
$secret,
$label
);
$totp = TOTP::create($secret);
$totp->setIssuer($issuer);
$totp->setLabel($label);

return $totp->getProvisioningUri();
}
}
26 changes: 17 additions & 9 deletions src/Forms/TOTPForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,49 @@

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));
}

/**
* @return FieldList|static
*/
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) {
Expand All @@ -56,14 +61,17 @@ public function getFormActions()
{
$action = FieldList::create(
[
FormAction::create('validateTOTP', 'Validate')
FormAction::create('validateTOTP', _t(self::class . 'TOTPVALIDATE', 'Validate'))
]
);

return $action;
}

/**
* 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()
Expand Down
Loading

0 comments on commit 0db94f8

Please sign in to comment.