PHP Classes

File: src/Discretion/Handlers/Register.php

Recommend this page to a friend!
  Classes of Scott Arciszewski   Discretion   src/Discretion/Handlers/Register.php   Download  
File: src/Discretion/Handlers/Register.php
Role: Class source
Content type: text/plain
Description: Class source
Class: Discretion
Show contact forms and deliver encrypted mail
Author: By
Last change:
Date: 2 years ago
Size: 8,060 bytes
 

Contents

Class file image Download
<?php
declare(strict_types=1);
namespace
ParagonIE\Discretion\Handlers;

use
Kelunik\TwoFactor\Oath;
use
ParagonIE\Discretion\Data\HiddenString;
use
ParagonIE\Discretion\Discretion;
use
ParagonIE\Discretion\Exception\DatabaseException;
use
ParagonIE\Discretion\Exception\SecurityException;
use
ParagonIE\Discretion\HandlerInterface;
use
ParagonIE\Discretion\Struct\User;
use
Psr\Http\Message\{
   
RequestInterface,
   
ResponseInterface
};
use
ReCaptcha\ReCaptcha;
use
Slim\Http\{
   
Request,
   
Response
};
use
ZxcvbnPhp\Zxcvbn;

/**
 * Class Index
 * @package ParagonIE\Discretion\Handlers
 */
class Register implements HandlerInterface
{
   
/**
     * @param RequestInterface $request
     * @param ResponseInterface $response
     * @param array $args
     * @return ResponseInterface
     * @throws \Error
     * @throws \Exception
     * @throws \ParagonIE\Discretion\Exception\DatabaseException
     * @throws \Twig_Error_Loader
     * @throws \Twig_Error_Runtime
     * @throws \Twig_Error_Syntax
     */
   
public function __invoke(
       
RequestInterface $request,
       
ResponseInterface $response,
        array
$args = []
    ):
ResponseInterface {
        if (
$request instanceof Request) {
            if (
$request->getAttribute('authenticated')) {
                return
Discretion::redirect('/manage');
            }
            if (
$request->isPost()) {
                try {
                    return
$this->processRegistrationRequest($request);
                } catch (
SecurityException $ex) {
                   
Discretion::setTwigVar('error', $ex->getMessage());
                   
// No. Fall through.
               
}
            }
        }
        if (!isset(
$_SESSION['registration'])) {
           
$_SESSION['registration'] = $this->initRegistration();
        }

       
// We're allowing CDNJS and Google for this page only:
       
Discretion::getCSPBuilder()
            ->
setSelfAllowed('style-src', true)
            ->
setAllowUnsafeInline('style-src', true)
            ->
addSource('connect-src', 'https://www.google.com')
            ->
addSource('child-src', 'https://www.google.com')
            ->
addSource('script-src', 'https://cdnjs.cloudflare.com')
            ->
addSource('script-src', 'https://www.google.com')
            ->
addSource('script-src', 'https://www.gstatic.com');

       
/** @var array<string, string> $reg */
       
$reg = $_SESSION['registration'];
       
/** @var string $twoFactorSecret */
       
$twoFactorSecret = $reg['twoFactorSecret'];

        return
Discretion::view(
           
'register.twig',
            [
               
'registration' => $_SESSION['registration'],
               
'qrcode' => (new Oath())->getUri(
                    (string)
$twoFactorSecret,
                    (string)
$_SERVER['HTTP_HOST'],
                   
'R_E_P_L_A_C_E_M_E'
               
)
            ]
        );
    }

   
/**
     * @return array
     */
   
protected function initRegistration(): array
    {
        return [
           
'twoFactorSecret' => (new Oath)->generateKey(32)
        ];
    }

   
/**
     * @param Request $request
     * @return Response
     * @throws DatabaseException
     * @throws SecurityException
     * @throws \Exception
     */
   
protected function processRegistrationRequest(Request $request): Response
   
{
        if (!
$request->getAttribute('csrf_mitigated')) {
            throw new
SecurityException('CSRF Mitigation not applied.');
        }

       
/** @var array<mixed, string> $post */
       
$post = $request->getParsedBody();

       
// Required fields
       
if (!isset(
           
$post['username'],
           
$post['email'],
           
$post['passphrase'],
           
$post['passphrase2'],
           
$post['twoFactor1'],
           
$post['twoFactor2']
        )) {
            throw new
SecurityException('Incomplete registration');
        }
       
/** @var array<string, string> $reg */
       
$reg = $_SESSION['registration'];

       
// Type checks
       
if (
            !\
is_string($post['username'])
            || !\
is_string($post['email'])
            || !\
is_string($post['passphrase'])
            || !\
is_string($post['passphrase2'])
            || !\
is_string($post['twoFactor1'])
            || !\
is_string($post['twoFactor2'])
            || !\
is_string($reg['twoFactorSecret'])
        ) {
            throw new
SecurityException('Invalid types');
        }
       
/** @var string $twoFactorSecret */
       
$twoFactorSecret = $reg['twoFactorSecret'];

       
// Validate the ReCAPTCHA response:
        /** @var array<string, array<string, string>> $settings */
       
$settings = Discretion::getSettings();
        if (!empty(
$settings['recaptcha']['secret-key'])) {
            if (empty(
$post['g-recaptcha-response'])) {
                throw new
SecurityException('Please complete the CAPTCHA.');
            }
           
$recaptcha = new ReCaptcha($settings['recaptcha']['secret-key']);
            if (!
$recaptcha->verify($post['g-recaptcha-response'], (string) ($_SERVER['REMOTE_ADDR']))) {
                throw new
SecurityException('Incorrect CAPTCHA response.');
            }
        }

       
// Ensure this is a valid email address.
        /** @var string $email */
       
$email = \filter_var($post['email'], FILTER_VALIDATE_EMAIL);
        if (!\
is_string($email)) {
            throw new
SecurityException('Invalid email address.');
        }

       
// If the username is already taken, do not allow it to be registered.
       
if (User::usernameIsTaken($post['username'])) {
            throw new
SecurityException('Username is already taken.');
        }

       
// Both passphrases need to match.
       
if (!\hash_equals($post['passphrase'], $post['passphrase2'])) {
            throw new
SecurityException('Passphrases do not match.');
        }

       
// Ensure the password is adequately strong.
       
$zxcvbn = new Zxcvbn();
       
$strength = $zxcvbn->passwordStrength($post['passphrase'],
            [
               
$post['username'],
               
$post['fullName'] ?? '',
               
$post['email']
            ]
        );
       
// Fail closed to a reasonably high value:
       
if (!isset($settings['zxcvbn-min-score'])) {
           
$settings['zxcvbn-min-score'] = 3;
        }
        if (
$strength['score'] < $settings['zxcvbn-min-score']) {
            throw new
SecurityException('Passphrase strength is inadequate.');
        }

       
// Ensure that the two 2FA responses are NOT identical.
       
if (\hash_equals($post['twoFactor1'], $post['twoFactor2'])) {
           
// The two cannot be the same:
           
throw new SecurityException('Incorrect two-factor authentication code.');
        }

       
// Verify two sequential 2FA codes generated from our 2FA secret:
       
$oath = new Oath();
        if (!
$oath->verifyTotp($twoFactorSecret, $post['twoFactor1'], 2)) {
            throw new
SecurityException('Incorrect two-factor authentication code.');
        }
        if (!
$oath->verifyTotp($twoFactorSecret, $post['twoFactor2'], 3)) {
            throw new
SecurityException('Incorrect two-factor authentication code.');
        }

       
// Story checks out. Let's create the user account.
       
$user = (new User())
            ->
setUsername($post['username'])
            ->
setEmail($post['email'])
            ->
setFullName($post['fullName'] ?? '')
            ->
setPassword(new HiddenString($post['passphrase']))
            ->
set2FASecret(new HiddenString($twoFactorSecret));

        if (!
$user->create()) {
            throw new
SecurityException('An unknown database error occurred.');
        }

       
Discretion::securityLog(
           
'User account created',
            [
               
'username' => $post['username']
            ]
        );

       
// Success: Regenerate session, set User ID.
       
unset($_SESSION['registration']);
        \
session_regenerate_id(true);
       
$_SESSION['userid'] = $user->id();

        return
Discretion::redirect('/manage');
    }
}