Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions config.example.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,9 @@
'admin' => 4,
'banned' => 5,
],

// Secure keys
'keys' => [
'tokens' => '',
],
];
52 changes: 52 additions & 0 deletions database/2018_06_06_004113_add_login_tables.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Miiverse\DB;

class AddLoginTables extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
$schema = DB::getSchemaBuilder();

$schema->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');
});
}
}
129 changes: 129 additions & 0 deletions src/Pages/Auth.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
<?php
/**
* Holds the auth controllers.
*/

namespace Miiverse\Pages;

use Miiverse\CurrentSession;
use Miiverse\DB;
use Miiverse\Net;
use Miiverse\User;
use Phroute\Phroute\Exception\HttpMethodNotAllowedException;

/**
* Authentication controllers.
*
* @author Repflez
*/
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');
}
}
52 changes: 52 additions & 0 deletions utility.php
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,55 @@ function __(string $key, array $replace = [])
{
return Translation::get($key, $replace);
}

function createToken(string $action, string $type = 'post') : array
{
$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];

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]);
}
}
}