PHP Classes

File: src/Discretion/Handlers/Login.php

Recommend this page to a friend!
  Packages of Scott Arciszewski   Discretion   src/Discretion/Handlers/Login.php   Download  
File: src/Discretion/Handlers/Login.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: 5 years ago
Size: 4,582 bytes
 

Contents

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

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

/**
 * Class Login
 * @package ParagonIE\Discretion\Handlers
 */
class Login implements HandlerInterface
{
    /**
     * @param RequestInterface $request
     * @param ResponseInterface $response
     * @param array $args
     * @return ResponseInterface
     * @throws \Error
     * @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->attemptLogin($request);
                } catch (\Throwable $ex) {
                    Discretion::setTwigVar('error', $ex->getMessage());
                }
            }
        }
        return Discretion::view('login.twig');
    }

    /**
     * Attempt to authenticate as this user.
     *
     * @param Request $request
     * @return Response
     * @throws SecurityException
     */
    protected function attemptLogin(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['passphrase'],
            $post['twoFactor']
        )) {
            throw new SecurityException('Incomplete login attempt.');
        }

        // Type checks
        if (
            !\is_string($post['username'])
            || !\is_string($post['passphrase'])
            || !\is_string($post['twoFactor'])
        ) {
            throw new SecurityException('Invalid HTTP message.');
        }

        $authStatus = true; // Set to false if any failures occur

        // To prevent trivial timing attacks:
        $dummyUser = (new User())
            ->setPassword(new HiddenString(random_bytes(32)))
            ->set2FASecret(new HiddenString(random_bytes(32)));
        try {
            $user = User::byUsername($post['username']);
        } catch (RecordNotFound $ex) {
            $authStatus = false;
            $user = $dummyUser;
        }

        // Validate the user's password (may be the dummy user):
        $authStatus = $authStatus && $user->checkPassword(
            new HiddenString($post['passphrase'])
        );

        // Two-factor authentication check:
        $oath = new Oath();
        $passwordSuccess = $authStatus;
        $authStatus = $authStatus && $oath->verifyTotp(
            $user->get2FASecret()->getString(),
            $post['twoFactor']
        );

        if (!$authStatus) {
            if ($passwordSuccess) {
                Discretion::securityLog(
                    'Correct password, incorrect 2FA code. Possible password breach.',
                    [
                        'username' => $post['username']
                    ],
                    Logger::WARNING
                );
            }
            // We used near-constant-time operations. Good luck figuring out which it was!
            throw new SecurityException(
                'Invalid username, password, or two-factor authentication challenge code.'
            );
        }
        Discretion::securityLog(
            'User successfully authenticated.',
            [
                'username' => $post['username']
            ],
            Logger::INFO
        );
        try {
            \session_regenerate_id(true);
            $_SESSION['user_id'] = $user->id();
        } catch (DatabaseException $ex) {
            throw new SecurityException(
                'Against all odds, you managed to guess the dummy user\'s password and 2FA secret.'
            );
        }
        return Discretion::redirect('/manage');
    }
}