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

Commit

Permalink
feat(Auth): Use JWT to set cookies content
Browse files Browse the repository at this point in the history
1. Static Cache for apps\components\Site->getBanIpsList()
2. Add hook when actionRateLimitCheckTrait valid fall
3. Use JWT to auth user, So we remove Constant::mapUserSessionToId
4. Fix `login_return_to` value which store in session, It will use
app()->request->fullUrl() now, so we can easily upgrade user to secure
connect
5. Sep Max User Login Session Number check as a callback rules
6. Add library `firebase/php-jwt`
7. Add user status check when loadCurUser()

BREAKING CHANGE: User status 'banned' replace by 'disabled'
  • Loading branch information
Rhilip committed Aug 10, 2019
1 parent 3680444 commit bf897c6
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 148 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@

### Refactor
- **Config:** Remove params `$throw` in Config()->get() (706cc9a)
- **RateLimit:** Change last param of isRateLimitHit and rate limit store Namespace (4dd571d)
- **View:** Make View extends BaseObject (0865cf9)
- **torrent/structure:** Use zui.tree instead javascript `$(this).next('ul').toggle()` (7b20b2c)
- **view:** Fix helper/username params (720f37e)


Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Or you can join our chat group on Telegram -- [@ridpt](https://t.me/ridpt)
| [MixPHP](https://github.com/mix-php/mix-framework/tree/v1) | Framework | <https://www.kancloud.cn/onanying/mixphp1/379324> ( Chinese Version ) |
| [siriusphp/validation](https://github.com/siriusphp/validation) | Validator | <http://www.sirius.ro/php/sirius/validation/> |
| [league/plates](https://github.com/thephpleague/plates) | Template system | <http://platesphp.com/> |
| [firebase/php-jwt](https://github.com/firebase/php-jwt) | JWT | <https://github.com/firebase/php-jwt>, <https://jwt.io/> |

## License
[![FOSSA Status](https://app.fossa.io/api/projects/git%2Bgithub.com%2FRhilip%2FRidPT.svg?type=large)](https://app.fossa.io/projects/git%2Bgithub.com%2FRhilip%2FRidPT?ref=badge_large)
Expand Down
103 changes: 80 additions & 23 deletions apps/components/Site.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@
use apps\libraries\Mailer;
use apps\libraries\Constant;

use Exception;
use Firebase\JWT\ExpiredException;
use Rid\Http\View;
use Rid\Base\Component;
use Rid\Utils\ClassValueCacheUtils;

use Firebase\JWT\JWT;
use RuntimeException;

class Site extends Component
{
use ClassValueCacheUtils;
Expand All @@ -35,6 +40,7 @@ public function onRequestBefore()
{
parent::onRequestBefore();
$this->cur_user = null;

$this->users = [];
$this->torrents = [];
$this->map_username_to_id = [];
Expand All @@ -47,9 +53,9 @@ protected static function getStaticCacheNameSpace(): string

public function getBanIpsList(): array
{
return $this->getCacheValue('ip_ban_list', function () {
return static::getStaticCacheValue('ip_ban_list', function () {
return app()->pdo->createCommand('SELECT `ip` FROM `ban_ips`')->queryColumn();
});
}, 86400);
}

public function getTorrent($tid)
Expand Down Expand Up @@ -120,42 +126,93 @@ public function getCurUser($grant = 'cookies')
protected function loadCurUser($grant = 'cookies')
{
$user_id = false;
if ($grant == 'cookies') $user_id = $this->loadCurUserFromCookies();
elseif ($grant == 'passkey') $user_id = $this->loadCurUserFromPasskey();
// elseif ($grant == 'oath2') $user_id = $this->loadCurUserFromOAth2();
if ($grant == 'cookies') $user_id = $this->loadCurUserIdFromCookies();
elseif ($grant == 'passkey') $user_id = $this->loadCurUserIdFromPasskey();
// elseif ($grant == 'oath2') $user_id = $this->loadCurUserIdFromOAth2();

if ($user_id !== false) {
$user_id = intval($user_id);
return $this->getUser($user_id);
$curuser = $this->getUser($user_id);
if ($curuser->getStatus() !== models\User::STATUS_DISABLED) // user status shouldn't be disabled
return $this->getUser($user_id);
}

return false;
}

protected function loadCurUserFromCookies()
protected function loadCurUserIdFromCookies()
{
$timenow = time();
$user_session_id = app()->request->cookie(Constant::cookie_name);
if (is_null($user_session_id)) return false; // quick return when cookies is not exist

if (false === $user_id = app()->redis->zScore(Constant::mapUserSessionToId, $user_session_id)) {
// First check cache
if (false === app()->redis->zScore(Constant::invalidUserSessionZset, $user_session_id)) {
// check session from database to avoid lost
$user_id = app()->pdo->createCommand('SELECT `uid` FROM `user_session_log` WHERE `sid` = :sid LIMIT 1;')->bindParams([
'sid' => $user_session_id
])->queryScalar();
if (false === $user_id) { // This session is not exist
app()->redis->zAdd(Constant::invalidUserSessionZset, time() + 86400, $user_session_id);
} else { // Remember it
app()->redis->zAdd(Constant::mapUserSessionToId, $user_id, $user_session_id);
$key = env('APP_SECRET_KEY');

try {
$decoded = JWT::decode($user_session_id, $key, ['HS256']);
} catch (Exception $e) {
if ($e instanceof ExpiredException) { // Lazy Expired Check .....
// Since in this case , we can't get payload with jti information directly, we should manually decode jwt content
list($headb64, $bodyb64, $cryptob64) = explode('.', $user_session_id);
$payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));
$jti = $payload->jti ?? '';
if ($jti && strlen($jti) === 64) {
app()->redis->zAdd(Constant::invalidUserSessionZset, $timenow + 86400, $jti);
app()->pdo->createCommand('UPDATE `user_session_log` SET `expired` = 1 WHERE `sid` = :sid')->bindParams([
'sid' => $jti
])->execute();
}
}
app()->session->set('jwt_error_msg', $e->getMessage()); // Store error msg
return false;
}

return $user_id;
$decoded_array = (array)$decoded; // jwt valid data in array

if (!isset($decoded_array['jti'])) return false;

// Check if user lock access ip ?
if (isset($decoded_array['secure_login_ip'])) {
$now_ip_crc = sprintf('%08x', crc32(app()->request->getClientIp()));
if (strcasecmp($decoded_array['secure_login_ip'], $now_ip_crc) !== 0) return false;
}

// Check if user want secure access but his environment is not secure
if (isset($decoded_array['ssl']) && $decoded_array['ssl'] && // User want secure access
!app()->request->isSecure() // User requests is not secure
// TODO our site support ssl feature
) {
app()->response->redirect(str_replace('http://', 'https://', app()->request->fullUrl()));
app()->response->setHeader('Strict-Transport-Security', 'max-age=1296000; includeSubDomains');
}

// Verity $jti is force expired or not ?
$jti = $decoded_array['jti'];
if ($jti !== app()->session->get('jti')) { // Not Match Session record
if (false === app()->redis->zScore(Constant::validUserSessionZset, $jti)) { // Not Record in valid cache
// if this $jti not in valid cache , then check invalid cache
if (app()->redis->zScore(Constant::invalidUserSessionZset, $jti) !== false) {
return false; // This $jti has been marked as invalid
} else { // Invalid cache still not hit, then check $jti from database to avoid lost
$exist_jti = app()->pdo->createCommand('SELECT `id` FROM `user_session_log` WHERE `sid` = :sid AND `expired` = 0 LIMIT 1;')->bindParams([
'sid' => $jti
])->queryScalar();

if (false === $exist_jti) { // Absolutely This $jti is not exist or expired
app()->redis->zAdd(Constant::invalidUserSessionZset, $timenow + 86400, $jti);
return false;
}
}

app()->redis->zAdd(Constant::validUserSessionZset, $decoded_array['exp'] ?? $timenow + 43200, $jti); // Store in valid cache
}
app()->session->set('jti', $jti); // Store the $jti value in session so we can visit $jti in other place
}

return $decoded_array['user_id'] ?? false;
}

protected function loadCurUserFromPasskey()
protected function loadCurUserIdFromPasskey()
{
$passkey = app()->request->get('passkey');
$user_id = app()->redis->zScore(Constant::mapUserPasskeyToId, $passkey);
Expand Down Expand Up @@ -225,11 +282,11 @@ public static function ruleCategory(): array

public static function CategoryDetail($cat_id): array
{
return static::getStaticCacheValue('torrent_category_' . $cat_id ,function () use ($cat_id) {
return static::getStaticCacheValue('torrent_category_' . $cat_id, function () use ($cat_id) {
return app()->pdo->createCommand('SELECT * FROM `categories` WHERE id= :cid LIMIT 1;')->bindParams([
'cid' => $cat_id
])->queryOne();
},86400);
}, 86400);
}

public static function ruleCanUsedCategory(): array
Expand All @@ -241,7 +298,7 @@ public static function ruleCanUsedCategory(): array

public static function ruleQuality($quality): array
{
if (!in_array($quality, array_keys(self::getQualityTableList()))) throw new \RuntimeException('Unregister quality : ' . $quality);
if (!in_array($quality, array_keys(self::getQualityTableList()))) throw new RuntimeException('Unregister quality : ' . $quality);
return static::getStaticCacheValue('enabled_quality_' . $quality, function () use ($quality) {
return app()->pdo->createCommand("SELECT * FROM `quality_$quality` WHERE `id` > 0 AND `enabled` = 1 ORDER BY `sort_index`,`id`")->queryAll();
}, 86400);
Expand Down
35 changes: 14 additions & 21 deletions apps/controllers/AuthController.php
Original file line number Diff line number Diff line change
Expand Up @@ -98,33 +98,26 @@ public function actionLogin()

if (app()->request->isPost()) {
$login = new Auth\UserLoginForm();
$login->setData(app()->request->post());
$success = $login->validate();

if (!$success) {
if (false === $success = $login->validate()) {
$login->LoginFail();
return $this->render('auth/login', [
"username" => $login->username,
"error_msg" => $login->getError(),
'username' => $login->username,
'error_msg' => $login->getError(),
'left_attempts' => $left_attempts
]);
} else {
$success = $login->createUserSession();
if ($success === true) {
$login->updateUserLoginInfo();

$return_to = app()->session->pop('login_return_to') ?? '/index';
if (!app()->request->isSecure() && $login->ssl === 'yes') { // Upgrade the scheme with full url
$return_to = 'https://' . app()->request->header('host') . $return_to;
}
$login->flush();

return app()->response->redirect($return_to);
} else {
return $this->render('action_fail', [
'title' => 'Login Failed',
'msg' => $success
]);
$return_to = app()->session->pop('login_return_to') ?? '/index';
if ($login->ssl === 'yes' // User want secure access
&& !app()->request->isSecure() // User requests is not secure
// && true // TODO our site support ssl feature
) { // Upgrade the scheme with full url
$return_to = str_replace('http://', 'https://', $return_to);
app()->response->setHeader('Strict-Transport-Security', 'max-age=1296000; includeSubDomains');
}

return app()->response->redirect($return_to);
}
} else {
return $this->render('auth/login', ['left_attempts' => $left_attempts]);
Expand All @@ -133,7 +126,7 @@ public function actionLogin()

public function actionLogout()
{
// TODO add CSRF protect
// TODO add CSRF protect and Logout Form
app()->site->getCurUser()->deleteUserThisSession();
return app()->response->redirect('/auth/login');
}
Expand Down
4 changes: 3 additions & 1 deletion apps/libraries/Constant.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ class Constant
const cookie_name = 'rid';

const mapUsernameToId = 'Map:user_username_to_user_id:hash';
const mapUserSessionToId = 'Map:user_session_to_user_id:zset';
const mapUserPasskeyToId = 'Map:user_passkey_to_user_id:zset';

// invalid Zset
const invalidUserIdZset = 'Site:invalid_user_id:zset';
const invalidUserSessionZset = 'Session:invalid_user_session:zset';
const invalidUserPasskeyZset = 'Tracker:invalid_user_passkey:zset';

// valid Zset
const validUserSessionZset = 'Session:valid_user_session:zset';

// Tracker Use
const trackerInvalidInfoHashZset = 'Tracker:invalid_torrent_info_hash:zset';
const trackerAllowedClientList = 'Tracker:allowed_client_list:string';
Expand Down
19 changes: 2 additions & 17 deletions apps/middleware/AuthByCookiesMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,10 @@ public function handle($callable, \Closure $next)
}

if (false === $curuser) {
$query = app()->request->server('query_string');
$to = app()->request->server('path_info') . (strlen($query) > 0 ? '?' . $query : '');
app()->session->set('login_return_to', $to);
app()->cookie->delete(Constant::cookie_name); // Delete exist cookies
app()->session->set('login_return_to', app()->request->fullUrl()); // Store the url which visitor want to hit
return app()->response->redirect('/auth/login');
} else {
/**
* Check if session is locked with IP
*/
$userSessionId = app()->request->cookie(Constant::cookie_name);
if (substr($userSessionId, 0, 1) === '1') {
$record_ip_crc = substr($userSessionId, 2, 8);
$this_ip_crc = sprintf('%08x', crc32($now_ip));

if (strcasecmp($record_ip_crc, $this_ip_crc) !== 0) { // The Ip isn't matched
app()->cookie->delete(Constant::cookie_name);
return app()->response->redirect('/auth/login');
}
}

/** Check User Permission to this route
*
* When user visit - /admin -> Controller : \apps\controllers\AdminController Action: actionIndex
Expand Down
17 changes: 12 additions & 5 deletions apps/models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ class User
];

// User Status
public const STATUS_BANNED = 'banned';
public const STATUS_DISABLED = 'disabled';
public const STATUS_PENDING = 'pending';
public const STATUS_PARKED = 'parked';
public const STATUS_CONFIRMED = 'confirmed';
Expand Down Expand Up @@ -137,6 +137,14 @@ public function getBonusOther()
return $this->bonus_other;
}

/**
* @return mixed
*/
public function getStatus()
{
return $this->status;
}

protected function getCacheNameSpace(): string
{
return Constant::userContent($this->id);
Expand Down Expand Up @@ -530,12 +538,11 @@ public function isPrivilege($require_class)
}

public function getSessionId() {
return app()->request->cookie(Constant::cookie_name);
return app()->session->get('jti');
}

public function deleteUserThisSession()
{
$user_session_id = app()->request->cookie(Constant::cookie_name);
public function deleteUserThisSession () { // FIXME
$user_session_id = app()->session->get('jti');
app()->pdo->createCommand('UPDATE `user_session_log` SET `expired` = 1 WHERE sid = :sid')->bindParams([
'sid' => $user_session_id
])->execute();
Expand Down
Loading

0 comments on commit bf897c6

Please sign in to comment.