Skip to content
This repository has been archived by the owner on Nov 2, 2020. It is now read-only.

Commit

Permalink
🔒 (Auth) Use UserLoginForm to manager user login action
Browse files Browse the repository at this point in the history
  • Loading branch information
Rhilip committed Jan 30, 2019
1 parent 9b15f76 commit 7dc131b
Show file tree
Hide file tree
Showing 3 changed files with 77 additions and 86 deletions.
57 changes: 14 additions & 43 deletions apps/controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@
namespace apps\controllers;

use apps\models\User;
use apps\models\form\UserLoginForm;
use apps\models\form\UserRegisterForm;

use Mix\Http\Controller;
use RobThree\Auth\TwoFactorAuth;


class AuthController extends Controller
Expand Down Expand Up @@ -59,46 +59,22 @@ public function actionRecover()
public function actionLogin()
{
if (app()->request->isPost()) {
$username = app()->request->post("username");
$self = app()->pdo->createCommand("SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1")->bindParams([
"uname" => $username, "email" => $username,
])->queryOne();
$login = new UserLoginForm();
$login->importAttributes(app()->request->post());
$error = $login->validate();

try {
// User is not exist
if (!$self) throw new \Exception("Invalid username/password");

// User's password is not correct
if (!password_verify(app()->request->post("password"), $self["password"]))
throw new \Exception("Invalid username/password");

// User enable 2FA but it's code is wrong
if ($self["opt"]) {
$tfa = new TwoFactorAuth(app()->config->get("base.site_name"));
if ($tfa->verifyCode($self["opt"], app()->request->post("opt")) == false)
throw new \Exception("2FA Validation failed");
}

// User 's status is banned or pending~
if (in_array($self["status"], ["banned", "pending"])) {
throw new \Exception("User account is not confirmed.");
if (count($error) > 0) {
return $this->render("auth/login.html.twig", ["username" => $login->username, "error_msg" => $error->get(0)]);
} else {
$success = $login->createUserSession();
if ($success) {
$login->updateUserLoginInfo();
$return_to = app()->session->pop('login_return_to') ?? '/index';
return app()->response->redirect($return_to);
} else {
return $this->render('errors/action_fail.html.twig',['title'=> 'Login Failed','msg' => 'Reach the limit of Max User Session.']);
}
} catch (\Exception $e) {
return $this->render("auth/login.html.twig", ["username" => $username, "error_msg" => $e->getMessage()]);
}

$success = app()->user->createUserSessionId($self["id"]);

if (!$success) {
return $this->render('errors/action_fail.html.twig',['title'=> 'Login Failed','msg' => 'Reach the limit of Max User Session.']);
}

app()->pdo->createCommand("UPDATE `users` SET `last_login_at` = NOW() , `last_login_ip` = INET6_ATON(:ip) WHERE `id` = :id")->bindParams([
"ip" => app()->request->getClientIp(), "id" => $self["id"]
])->execute();

$return_to = app()->session->pop('login_return_to') ?? '/index';
return app()->response->redirect($return_to);
} else {
return $this->render("auth/login.html.twig");
}
Expand All @@ -110,9 +86,4 @@ public function actionLogout()
app()->user->deleteUserThisSession();
return app()->response->redirect('/auth/login');
}

private function isMaxLoginIpReached()
{

}
}
66 changes: 55 additions & 11 deletions apps/models/form/UserLoginForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@

namespace apps\models\form;

use Mix\Helpers\StringHelper;
use Mix\User\UserInterface;
use Mix\Validators\Validator;

use RobThree\Auth\TwoFactorAuth;
use RobThree\Auth\TwoFactorAuthException;

use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Context\ExecutionContextInterface;
use Symfony\Component\Validator\Mapping\ClassMetadata;
Expand All @@ -25,6 +27,26 @@ class UserLoginForm extends Validator

private $self;

// Key Information of User Session
private $sessionLength = 26;
private $sessionSaveKey = 'SESSION:user_set';

// Cookie
private $cookieName = 'rid';
private $cookieExpires = 0x7fffffff;
private $cookiePath = '/';
private $cookieDomain = '';
private $cookieSecure = false;
private $cookieHttpOnly = true;

public function __construct(array $config = [])
{
parent::__construct($config);
$this->sessionSaveKey = app()->user->sessionSaveKey;
$this->cookieName = app()->user->cookieName;
}


public static function rules()
{
return [
Expand All @@ -49,50 +71,72 @@ public static function loadValidatorMetadata(ClassMetadata $metadata)
foreach ($rules as $property => $constraints) {
$metadata->addPropertyConstraints($property, $constraints);
}
$metadata->addConstraint(new Assert\Callback('loadUserFromPDO'));
$metadata->addConstraint(new Assert\Callback('loadUserFromPdo'));
$metadata->addConstraint(new Assert\Callback('isMaxLoginIpReached'));
}

public function loadUserFromPDO(ExecutionContextInterface $context)
public function loadUserFromPdo(ExecutionContextInterface $context)
{
$this->self = app()->pdo->createCommand("SELECT `id`,`username`,`password`,`status`,`opt`,`class` from users WHERE `username` = :uname OR `email` = :email LIMIT 1")->bindParams([
"uname" => $this->username, "email" => $this->username,
])->queryOne();

if (!$this->self) // User is not exist
if (!$this->self) { // User is not exist
$context->buildViolation("This User is not exist in this site.")->addViolation();
return;
}

// User's password is not correct
if (!password_verify($this->password, $this->self["password"]))
if (!password_verify($this->password, $this->self["password"])) {
$context->buildViolation("Invalid username/password")->addViolation();
return;
}

// User enable 2FA but it's code is wrong
if ($this->self["opt"]) {
try {
$tfa = new TwoFactorAuth(app()->config->get("base.site_name"));
if ($tfa->verifyCode($this->self["opt"], $this->opt) == false)
if ($tfa->verifyCode($this->self["opt"], $this->opt) == false) {
$context->buildViolation("2FA Validation failed")->addViolation();
return;
}
} catch (TwoFactorAuthException $e) {
$context->buildViolation("2FA Validation failed")->addViolation();
return;
}
}

// User 's status is banned or pending~
if (in_array($this->self["status"], [UserInterface::STATUS_BANNED, UserInterface::STATUS_PENDING])) {
$context->buildViolation("User account is not confirmed.")->addViolation();
return;
}
}

public function createUserSession()
public function isMaxLoginIpReached()
{
app()->user->createUserSessionId();
app()->user->set('user', $this->self);
// TODO Check User Fail Login Ip Count
}

// TODO record user login session

$this->updateUserLogin();
public function createUserSession()
{
$userId = $this->self['id'];
$userSessionId = StringHelper::getRandomString($this->sessionLength);

$exist_session_count = app()->redis->zCount($this->sessionSaveKey, $userId, $userId);
if ($exist_session_count < app()->config->get('base.max_per_user_session')) {
app()->redis->zAdd($this->sessionSaveKey, $userId, $userSessionId);
// TODO store user login information , ( for example `login ip`,`platform`,`browser`,`last activity at` )
app()->response->setCookie($this->cookieName, $userSessionId, $this->cookieExpires, $this->cookiePath, $this->cookieDomain, $this->cookieSecure, $this->cookieHttpOnly);
return true;
} else {
return false;
}
}

private function updateUserLogin() {
public function updateUserLoginInfo()
{
app()->pdo->createCommand("UPDATE `users` SET `last_login_at` = NOW() , `last_login_ip` = INET6_ATON(:ip) WHERE `id` = :id")->bindParams([
"ip" => app()->request->getClientIp(), "id" => $this->self["id"]
])->execute();
Expand Down
40 changes: 8 additions & 32 deletions framework/User/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,33 +9,25 @@
namespace Mix\User;

use Mix\Base\Component;
use Mix\Helpers\StringHelper;


class User extends Component implements UserInterface
{
use UserTrait;

public $sessionSaveKey = 'USER_SESSION';

// Cookie
public $sessionSaveKey = 'SESSION:user_set';
public $cookieName = 'rid';
public $cookieExpires = 0x7fffffff;
public $cookiePath = '/';
public $cookieDomain = '';
public $cookieSecure = false;
public $cookieHttpOnly = false;

// Key of User Session
protected $_userKey;
protected $_userSessionId;
protected $_userIdLength = 26;

private $anonymous = false;

public function onRequestBefore()
{
parent::onRequestBefore();
$this->loadUserFromCookies();
$this->loadUser();
}

public function onRequestAfter()
Expand All @@ -49,6 +41,11 @@ public function isAnonymous()
return $this->anonymous;
}

public function loadUser() {
// TODO Load User From Passkey in some route , for example '/rss'
return $this->loadUserFromCookies();
}

public function loadUserFromCookies()
{
$this->_userSessionId = \Mix::app()->request->cookie($this->cookieName);
Expand All @@ -62,34 +59,13 @@ public function loadUserFromCookies()
}
}

public function createUserSessionId($userId)
{
$this->_userSessionId = StringHelper::getRandomString($this->_userIdLength);

$exist_session_count = app()->redis->zCount($this->sessionSaveKey, $userId, $userId);
if ($exist_session_count < app()->config->get('base.max_per_user_session')) {
app()->redis->zAdd($this->sessionSaveKey, $userId, $this->_userSessionId);
app()->cookie->set($this->cookieName, $this->_userSessionId);
return true;
} else {
return false;
}
}

public function deleteUserThisSession()
{
$success = app()->redis->zRem($this->sessionSaveKey, $this->_userSessionId);
app()->cookie->delete($this->cookieName);
return $success ? true : false;
}

// 删除
public function delete($name)
{
$success = app()->redis->zRem($this->sessionSaveKey, $name);
return $success ? true : false;
}

// 获取SessionId
public function getSessionId()
{
Expand Down

0 comments on commit 7dc131b

Please sign in to comment.