From a6b2d72988d5b583bec988a637e7cfbcabcd9cd3 Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 00:42:30 -0600 Subject: [PATCH 1/9] Add login tables for the web sessions implementation --- .../2018_06_06_004113_add_login_tables.php | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 database/2018_06_06_004113_add_login_tables.php diff --git a/database/2018_06_06_004113_add_login_tables.php b/database/2018_06_06_004113_add_login_tables.php new file mode 100644 index 0000000..2037d00 --- /dev/null +++ b/database/2018_06_06_004113_add_login_tables.php @@ -0,0 +1,52 @@ +create('login_attempts', function (Blueprint $table) { + $table->increments('attempt_id'); + + $table->tinyInteger('attempt_success') + ->unsigned(); + + $table->integer('attempt_timestamp') + ->unsigned(); + + $table->binary('attempt_ip'); + + $table->integer('user_id') + ->unsigned(); + }); + + $schema->table('users', function (Blueprint $table) { + $table->string('password') + ->nullable(true); + }); + } + + /** + * Reverse the migrations. + * + * @return void + */ + public function down() + { + $schema = DB::getSchemaBuilder(); + + $schema->table('users', function (Blueprint $table) { + $table->dropColumn('password'); + }); + } +} From 0479eb8c628fdea89ac0d1d23a6511569e5948ac Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 00:48:38 -0600 Subject: [PATCH 2/9] Add form token generation functions. From SMF2 code. --- utility.php | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/utility.php b/utility.php index 4395a56..c75ef6d 100644 --- a/utility.php +++ b/utility.php @@ -265,3 +265,51 @@ function dehashid($hash) { return Hashid::decode($hash); } + +function createToken(string $action, string $type = 'post') : array +{ + $token = md5(mt_rand() . session_id() . (string) microtime() . config('keys.nonce') . config('salts.nonce') . $type); + $token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand() . (string) microtime() . mt_rand())), 0, mt_rand(7, 12)); + + $_SESSION['token'][$type . '-' . $action] = [$token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token]; + + return [$action . '_token_var' => $token_var, $action . '_token' => $token]; +} + +function validateToken(string $action, string $type = 'post', bool $reset = true) +{ + $type = $type == 'get' || $type == 'request' ? $type : 'post'; + + if (isset($_SESSION['token'][$type . '-' . $action], $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]]) && md5($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['token'][$type . '-' . $action][1]) { + // Invalidate this token now. + unset($_SESSION['token'][$type . '-' . $action]); + + return true; + } + + if ($reset) { + cleanTokens(); + + createToken($action, $type); + + http_response_code(400); + die('Invalid token'); + } + else + unset($_SESSION['token'][$type . '-' . $action]); + + if (mt_rand(0, 138) == 23) + cleanTokens(); + + return false; +} + +function cleanTokens(bool $complete = false) : void +{ + if (!isset($_SESSION['token'])) + return; + + foreach ($_SESSION['token'] as $key => $data) + if ($data[2] + 10800 < time() || $complete) + unset($_SESSION['token'][$key]); +} \ No newline at end of file From 92f66f46cf4f79cf3e49ccfe336b54722f4be0de Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 00:59:53 -0600 Subject: [PATCH 3/9] StyleCI fixes --- utility.php | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/utility.php b/utility.php index c75ef6d..2d3ab8b 100644 --- a/utility.php +++ b/utility.php @@ -268,21 +268,21 @@ function dehashid($hash) function createToken(string $action, string $type = 'post') : array { - $token = md5(mt_rand() . session_id() . (string) microtime() . config('keys.nonce') . config('salts.nonce') . $type); - $token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand() . (string) microtime() . mt_rand())), 0, mt_rand(7, 12)); + $token = md5(mt_rand().session_id().(string) microtime().config('keys.nonce').config('salts.nonce').$type); + $token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand().(string) microtime().mt_rand())), 0, mt_rand(7, 12)); - $_SESSION['token'][$type . '-' . $action] = [$token_var, md5($token . $_SERVER['HTTP_USER_AGENT']), time(), $token]; + $_SESSION['token'][$type.'-'.$action] = [$token_var, md5($token.$_SERVER['HTTP_USER_AGENT']), time(), $token]; - return [$action . '_token_var' => $token_var, $action . '_token' => $token]; + return [$action.'_token_var' => $token_var, $action.'_token' => $token]; } function validateToken(string $action, string $type = 'post', bool $reset = true) { $type = $type == 'get' || $type == 'request' ? $type : 'post'; - if (isset($_SESSION['token'][$type . '-' . $action], $GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]]) && md5($GLOBALS['_' . strtoupper($type)][$_SESSION['token'][$type . '-' . $action][0]] . $_SERVER['HTTP_USER_AGENT']) == $_SESSION['token'][$type . '-' . $action][1]) { + if (isset($_SESSION['token'][$type.'-'.$action], $GLOBALS['_'.strtoupper($type)][$_SESSION['token'][$type.'-'.$action][0]]) && md5($GLOBALS['_'.strtoupper($type)][$_SESSION['token'][$type.'-'.$action][0]].$_SERVER['HTTP_USER_AGENT']) == $_SESSION['token'][$type.'-'.$action][1]) { // Invalidate this token now. - unset($_SESSION['token'][$type . '-' . $action]); + unset($_SESSION['token'][$type.'-'.$action]); return true; } @@ -294,22 +294,26 @@ function validateToken(string $action, string $type = 'post', bool $reset = true http_response_code(400); die('Invalid token'); + } else { + unset($_SESSION['token'][$type.'-'.$action]); } - else - unset($_SESSION['token'][$type . '-' . $action]); - if (mt_rand(0, 138) == 23) + if (mt_rand(0, 138) == 23) { cleanTokens(); + } return false; } function cleanTokens(bool $complete = false) : void { - if (!isset($_SESSION['token'])) + if (!isset($_SESSION['token'])) { return; + } - foreach ($_SESSION['token'] as $key => $data) - if ($data[2] + 10800 < time() || $complete) + foreach ($_SESSION['token'] as $key => $data) { + if ($data[2] + 10800 < time() || $complete) { unset($_SESSION['token'][$key]); + } + } } \ No newline at end of file From 4f3d35afb2ecd84e8cc813c86a732ee6305ed9ac Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:00:48 -0600 Subject: [PATCH 4/9] Forgot the newline... --- utility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utility.php b/utility.php index 2d3ab8b..fa99737 100644 --- a/utility.php +++ b/utility.php @@ -316,4 +316,4 @@ function cleanTokens(bool $complete = false) : void unset($_SESSION['token'][$key]); } } -} \ No newline at end of file +} From 1b22828e68646948c0ecf64acf7ffd443d5133f0 Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:01:47 -0600 Subject: [PATCH 5/9] Add secure keys section to config --- config.example.php | 5 +++++ utility.php | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/config.example.php b/config.example.php index 8bcd40a..f87ddf1 100644 --- a/config.example.php +++ b/config.example.php @@ -110,4 +110,9 @@ 'admin' => 4, 'banned' => 5, ], + + // Secure keys + 'keys' => [ + 'tokens' => '', + ], ]; diff --git a/utility.php b/utility.php index fa99737..ff43eb7 100644 --- a/utility.php +++ b/utility.php @@ -268,7 +268,7 @@ function dehashid($hash) function createToken(string $action, string $type = 'post') : array { - $token = md5(mt_rand().session_id().(string) microtime().config('keys.nonce').config('salts.nonce').$type); + $token = md5(mt_rand().session_id().(string) microtime().config('keys.tokens').$type); $token_var = substr(preg_replace('~^\d+~', '', md5(mt_rand().(string) microtime().mt_rand())), 0, mt_rand(7, 12)); $_SESSION['token'][$type.'-'.$action] = [$token_var, md5($token.$_SERVER['HTTP_USER_AGENT']), time(), $token]; From 8c2b8afe9862e6fe49b7a485b551398bd97ad5b8 Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:11:23 -0600 Subject: [PATCH 6/9] Add code for the web auth page --- src/Pages/Auth.php | 118 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 src/Pages/Auth.php diff --git a/src/Pages/Auth.php b/src/Pages/Auth.php new file mode 100644 index 0000000..0c421df --- /dev/null +++ b/src/Pages/Auth.php @@ -0,0 +1,118 @@ +insert([ + 'attempt_success' => $success ? 1 : 0, + 'attempt_timestamp' => time(), + 'attempt_ip' => Net::pton(Net::ip()), + 'user_id' => $user, + ]); + } + + protected function authenticate(User $user) : void { + // Generate a session key + $session = CurrentSession::create( + $user->id, + Net::ip(), + get_country_code() + ); + + $cookiePrefix = config('cookie.prefix'); + setcookie("{$cookiePrefix}id", $user->id, time() + 604800, '/'); + setcookie("{$cookiePrefix}session", $session->key, time() + 604800, '/'); + + $this->touchRateLimit($user->id, true); + } + + /** + * End the current session. + * @throws HttpMethodNotAllowedException + */ + public function logout() { + if (!session_check('z')) { + throw new HttpMethodNotAllowedException; + } + + // Destroy the active session + CurrentSession::stop(); + + return redirect(route('main.index')); + } + + /** + * Login page. + * @return string + */ + public function login() : string { + if (!validateToken('login', 'post', false)) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_token'))); + } + + // Get request variables + $username = $_POST['username'] ?? null; + $password = $_POST['password'] ?? null; + + // Check if we haven't hit the rate limit + $rates = DB::table('login_attempts')->where('attempt_ip', Net::pton(Net::ip()))->where('attempt_timestamp', '>', time() - 1800)->where('attempt_success', '0')->count(); + + if ($rates > 4) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.too_many_attempts'))); + } + + $user = User::construct(clean_string($username, true)); + + // Check if the user that's trying to log in actually exists + if ($user->id === 0) { + $this->touchRateLimit($user->id); + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_details'))); + } + + if ($user->passwordExpired()) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.password_expired'))); + } + + if (!$user->verifyPassword($password)) { + $this->touchRateLimit($user->id); + return redirect(route('auth.loginform') . '?mes=' . __('auth.errors.invalid_details')); + } + + $this->authenticate($user); + + return redirect(route('main.index')); + } + + /** + * Serve the login form + * @return string + */ + public function login_form() : string { + if (CurrentSession::$user->id !== 0) { + return redirect(route('main.index')); + } + + return view('members/login'); + } +} From 3d1c5c7b751c5782a1f020ac4d6d44e733695384 Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:12:43 -0600 Subject: [PATCH 7/9] eek line endings --- src/Pages/Auth.php | 196 ++++++++++++++++++++++----------------------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/src/Pages/Auth.php b/src/Pages/Auth.php index 0c421df..690d8a3 100644 --- a/src/Pages/Auth.php +++ b/src/Pages/Auth.php @@ -5,11 +5,11 @@ namespace Miiverse\Pages; -use Phroute\Phroute\Exception\HttpMethodNotAllowedException; use Miiverse\CurrentSession; use Miiverse\DB; use Miiverse\Net; use Miiverse\User; +use Phroute\Phroute\Exception\HttpMethodNotAllowedException; /** * Authentication controllers. @@ -18,101 +18,101 @@ */ class Auth extends Page { - /** - * Touch the login rate limit. - * @param int $user The ID of the user that attempted to log in. - * @param bool $success Whether the login attempt was successful. - */ - protected function touchRateLimit(int $user, bool $success = false) : void { - DB::table('login_attempts')->insert([ - 'attempt_success' => $success ? 1 : 0, - 'attempt_timestamp' => time(), - 'attempt_ip' => Net::pton(Net::ip()), - 'user_id' => $user, - ]); - } - - protected function authenticate(User $user) : void { - // Generate a session key - $session = CurrentSession::create( - $user->id, - Net::ip(), - get_country_code() - ); - - $cookiePrefix = config('cookie.prefix'); - setcookie("{$cookiePrefix}id", $user->id, time() + 604800, '/'); - setcookie("{$cookiePrefix}session", $session->key, time() + 604800, '/'); - - $this->touchRateLimit($user->id, true); - } - - /** - * End the current session. - * @throws HttpMethodNotAllowedException - */ - public function logout() { - if (!session_check('z')) { - throw new HttpMethodNotAllowedException; - } - - // Destroy the active session - CurrentSession::stop(); - - return redirect(route('main.index')); - } - - /** - * Login page. - * @return string - */ - public function login() : string { - if (!validateToken('login', 'post', false)) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_token'))); - } - - // Get request variables - $username = $_POST['username'] ?? null; - $password = $_POST['password'] ?? null; - - // Check if we haven't hit the rate limit - $rates = DB::table('login_attempts')->where('attempt_ip', Net::pton(Net::ip()))->where('attempt_timestamp', '>', time() - 1800)->where('attempt_success', '0')->count(); - - if ($rates > 4) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.too_many_attempts'))); - } - - $user = User::construct(clean_string($username, true)); - - // Check if the user that's trying to log in actually exists - if ($user->id === 0) { - $this->touchRateLimit($user->id); - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_details'))); - } - - if ($user->passwordExpired()) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.password_expired'))); - } - - if (!$user->verifyPassword($password)) { - $this->touchRateLimit($user->id); - return redirect(route('auth.loginform') . '?mes=' . __('auth.errors.invalid_details')); - } - - $this->authenticate($user); - - return redirect(route('main.index')); - } - - /** - * Serve the login form - * @return string - */ - public function login_form() : string { - if (CurrentSession::$user->id !== 0) { - return redirect(route('main.index')); - } - - return view('members/login'); - } + /** + * Touch the login rate limit. + * @param int $user The ID of the user that attempted to log in. + * @param bool $success Whether the login attempt was successful. + */ + protected function touchRateLimit(int $user, bool $success = false) : void { + DB::table('login_attempts')->insert([ + 'attempt_success' => $success ? 1 : 0, + 'attempt_timestamp' => time(), + 'attempt_ip' => Net::pton(Net::ip()), + 'user_id' => $user, + ]); + } + + protected function authenticate(User $user) : void { + // Generate a session key + $session = CurrentSession::create( + $user->id, + Net::ip(), + get_country_code() + ); + + $cookiePrefix = config('cookie.prefix'); + setcookie("{$cookiePrefix}id", $user->id, time() + 604800, '/'); + setcookie("{$cookiePrefix}session", $session->key, time() + 604800, '/'); + + $this->touchRateLimit($user->id, true); + } + + /** + * End the current session. + * @throws HttpMethodNotAllowedException + */ + public function logout() { + if (!session_check('z')) { + throw new HttpMethodNotAllowedException; + } + + // Destroy the active session + CurrentSession::stop(); + + return redirect(route('main.index')); + } + + /** + * Login page. + * @return string + */ + public function login() : string { + if (!validateToken('login', 'post', false)) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_token'))); + } + + // Get request variables + $username = $_POST['username'] ?? null; + $password = $_POST['password'] ?? null; + + // Check if we haven't hit the rate limit + $rates = DB::table('login_attempts')->where('attempt_ip', Net::pton(Net::ip()))->where('attempt_timestamp', '>', time() - 1800)->where('attempt_success', '0')->count(); + + if ($rates > 4) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.too_many_attempts'))); + } + + $user = User::construct(clean_string($username, true)); + + // Check if the user that's trying to log in actually exists + if ($user->id === 0) { + $this->touchRateLimit($user->id); + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_details'))); + } + + if ($user->passwordExpired()) { + return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.password_expired'))); + } + + if (!$user->verifyPassword($password)) { + $this->touchRateLimit($user->id); + return redirect(route('auth.loginform') . '?mes=' . __('auth.errors.invalid_details')); + } + + $this->authenticate($user); + + return redirect(route('main.index')); + } + + /** + * Serve the login form + * @return string + */ + public function login_form() : string { + if (CurrentSession::$user->id !== 0) { + return redirect(route('main.index')); + } + + return view('members/login'); + } } From ead9d9e00c6e4e29bfc0d5f8d84900a5a6a6339c Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:16:04 -0600 Subject: [PATCH 8/9] StyleCI fixes --- src/Pages/Auth.php | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/src/Pages/Auth.php b/src/Pages/Auth.php index 690d8a3..13ebbcb 100644 --- a/src/Pages/Auth.php +++ b/src/Pages/Auth.php @@ -20,19 +20,22 @@ class Auth extends Page { /** * Touch the login rate limit. - * @param int $user The ID of the user that attempted to log in. + * + * @param int $user The ID of the user that attempted to log in. * @param bool $success Whether the login attempt was successful. */ - protected function touchRateLimit(int $user, bool $success = false) : void { + protected function touchRateLimit(int $user, bool $success = false) : void + { DB::table('login_attempts')->insert([ - 'attempt_success' => $success ? 1 : 0, + 'attempt_success' => $success ? 1 : 0, 'attempt_timestamp' => time(), - 'attempt_ip' => Net::pton(Net::ip()), - 'user_id' => $user, + 'attempt_ip' => Net::pton(Net::ip()), + 'user_id' => $user, ]); } - protected function authenticate(User $user) : void { + protected function authenticate(User $user) : void + { // Generate a session key $session = CurrentSession::create( $user->id, @@ -49,11 +52,13 @@ protected function authenticate(User $user) : void { /** * End the current session. + * * @throws HttpMethodNotAllowedException */ - public function logout() { + public function logout() + { if (!session_check('z')) { - throw new HttpMethodNotAllowedException; + throw new HttpMethodNotAllowedException(); } // Destroy the active session @@ -64,11 +69,13 @@ public function logout() { /** * Login page. + * * @return string */ - public function login() : string { + public function login() : string + { if (!validateToken('login', 'post', false)) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_token'))); + return redirect(route('auth.loginform').'?mes='.urlencode(__('auth.errors.invalid_token'))); } // Get request variables @@ -79,7 +86,7 @@ public function login() : string { $rates = DB::table('login_attempts')->where('attempt_ip', Net::pton(Net::ip()))->where('attempt_timestamp', '>', time() - 1800)->where('attempt_success', '0')->count(); if ($rates > 4) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.too_many_attempts'))); + return redirect(route('auth.loginform').'?mes='.urlencode(__('auth.errors.too_many_attempts'))); } $user = User::construct(clean_string($username, true)); @@ -87,16 +94,16 @@ public function login() : string { // Check if the user that's trying to log in actually exists if ($user->id === 0) { $this->touchRateLimit($user->id); - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.invalid_details'))); + return redirect(route('auth.loginform').'?mes='.urlencode(__('auth.errors.invalid_details'))); } if ($user->passwordExpired()) { - return redirect(route('auth.loginform') . '?mes=' . urlencode(__('auth.errors.password_expired'))); + return redirect(route('auth.loginform').'?mes='.urlencode(__('auth.errors.password_expired'))); } if (!$user->verifyPassword($password)) { $this->touchRateLimit($user->id); - return redirect(route('auth.loginform') . '?mes=' . __('auth.errors.invalid_details')); + return redirect(route('auth.loginform').'?mes='.__('auth.errors.invalid_details')); } $this->authenticate($user); @@ -106,9 +113,11 @@ public function login() : string { /** * Serve the login form + * * @return string */ - public function login_form() : string { + public function login_form() : string + { if (CurrentSession::$user->id !== 0) { return redirect(route('main.index')); } From a2dedf832ab3903095c9a8b55074811c7ebcbf75 Mon Sep 17 00:00:00 2001 From: Repflez Date: Wed, 6 Jun 2018 01:16:45 -0600 Subject: [PATCH 9/9] :eyes: --- src/Pages/Auth.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Pages/Auth.php b/src/Pages/Auth.php index 13ebbcb..958d882 100644 --- a/src/Pages/Auth.php +++ b/src/Pages/Auth.php @@ -94,6 +94,7 @@ public function login() : string // Check if the user that's trying to log in actually exists if ($user->id === 0) { $this->touchRateLimit($user->id); + return redirect(route('auth.loginform').'?mes='.urlencode(__('auth.errors.invalid_details'))); } @@ -103,6 +104,7 @@ public function login() : string if (!$user->verifyPassword($password)) { $this->touchRateLimit($user->id); + return redirect(route('auth.loginform').'?mes='.__('auth.errors.invalid_details')); } @@ -112,7 +114,7 @@ public function login() : string } /** - * Serve the login form + * Serve the login form. * * @return string */