diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48b8bf9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +vendor/ diff --git a/app/config/config.php b/app/config/config.php new file mode 100644 index 0000000..22c03f3 --- /dev/null +++ b/app/config/config.php @@ -0,0 +1,20 @@ + diff --git a/app/controller/UserController.php b/app/controller/UserController.php new file mode 100644 index 0000000..3e537aa --- /dev/null +++ b/app/controller/UserController.php @@ -0,0 +1,36 @@ +password)) { + $user->login_attempts = 0; + $user->save(); + return $user; + } else { + $user->login_attempts++; + $user->save(); + return null; + } + } + + public static function create($request) { + $username = htmlspecialchars($request['username']); + $password = htmlspecialchars($request['password']); + $user = new User; + $user->username = $username; + $user->password = password_hash($password, PASSWORD_DEFAULT); + $user->save(); + } +} +?> diff --git a/app/database/Database.php b/app/database/Database.php new file mode 100644 index 0000000..e04e87e --- /dev/null +++ b/app/database/Database.php @@ -0,0 +1,10 @@ + diff --git a/app/database/init.php b/app/database/init.php new file mode 100644 index 0000000..f523308 --- /dev/null +++ b/app/database/init.php @@ -0,0 +1,38 @@ +exec('CREATE DATABASE IF NOT EXISTS ' . Config::db_name); + +/* Connect to database */ +$dbh = Database::connect(); + +/* Create users table if it doesn't exist */ +$stmt = 'CREATE TABLE IF NOT EXISTS users ( + id INT(6) UNSIGNED AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(32) NOT NULL, + password VARCHAR(64) NOT NULL, + access VARCHAR(8) NOT NULL, + login_attempts INT(6) UNSIGNED NOT NULL +)'; +$dbh->exec($stmt); + +/* Create user and admin if they don't exist */ +$user = new User; +$user->username = 'user'; +$user->password = password_hash('userpass', PASSWORD_DEFAULT); +$user->access = 'user'; +$user->save(); + +$user = new User; +$user->username = 'admin'; +$user->password = password_hash('adminpass', PASSWORD_DEFAULT); +$user->access = 'admin'; +$user->save(); + +?> diff --git a/app/forms/change_password.php b/app/forms/change_password.php new file mode 100644 index 0000000..aa1190d --- /dev/null +++ b/app/forms/change_password.php @@ -0,0 +1,41 @@ + + +
+
+
+
+ +
+
+
+
+ lock + + +
+
+ lock + + +
+ + +
+
+
+
+
+
+
diff --git a/app/forms/create.php b/app/forms/create.php new file mode 100644 index 0000000..2acc73d --- /dev/null +++ b/app/forms/create.php @@ -0,0 +1,39 @@ + + + diff --git a/app/forms/edit.php b/app/forms/edit.php new file mode 100644 index 0000000..0b50ace --- /dev/null +++ b/app/forms/edit.php @@ -0,0 +1,40 @@ + + diff --git a/app/forms/login.php b/app/forms/login.php new file mode 100644 index 0000000..80be14f --- /dev/null +++ b/app/forms/login.php @@ -0,0 +1,43 @@ + + +
+
+
+
+ +
+
+
+
+ account_circle + + +
+
+ lock + + +
+ + +
+
+
+
+
+
+
diff --git a/app/include/http.php b/app/include/http.php new file mode 100644 index 0000000..e3696e2 --- /dev/null +++ b/app/include/http.php @@ -0,0 +1,24 @@ + diff --git a/app/model/User.php b/app/model/User.php new file mode 100644 index 0000000..606b228 --- /dev/null +++ b/app/model/User.php @@ -0,0 +1,106 @@ +username)) { + $stmt = $dbh->prepare("UPDATE users SET password=:password, access=:access, login_attempts=:attempts WHERE username=:username"); + } else { + $stmt = $dbh->prepare("INSERT INTO users (username, password, access, login_attempts) VALUES (:username, :password, :access, :attempts)"); + } + $stmt->bindParam(':username', $this->username); + $stmt->bindParam(':password', $this->password); + $stmt->bindParam(':access', $this->access); + $stmt->bindParam(':attempts', $this->login_attempts); + $stmt->execute(); + $this->id = $dbh->lastInsertId(); + } + + public function delete() { + $dbh = Database::connect(); + $stmt = $dbh->prepare("DELETE FROM users WHERE id=:id"); + $stmt->bindParam(':id', $this->id); + $stmt->execute(); + } + + public static function exists($username) { + $dbh = Database::connect(); + $stmt = $dbh->prepare("SELECT count(*) FROM users WHERE username = :username"); + $stmt->bindParam(':username', $username); + $stmt->execute(); + return $stmt->fetchColumn() > 0; + } + + public static function get($username) { + $users = User::all(); + foreach ($users as $user) { + if ($user->username == $username) { + return $user; + } + } + return null; + } + + public static function all() { + $dbh = Database::connect(); + $stmt = $dbh->prepare("SELECT * from users"); + $stmt->execute(); + return array_map(function ($row) { + $user = new User; + $user->id = $row['id']; + $user->username = $row['username']; + $user->password = $row['password']; + $user->access = $row['access']; + $user->login_attempts = $row['login_attempts']; + return $user; + }, $stmt->fetchAll()); + } + + public static function authenticated() { + try { + $jwt = Http::cookie(Config::cookie_name); + if (!$jwt) return null; + $token = JWT::decode($jwt, base64_encode(Config::secret_key), ['HS512']); + return $token->data; + } catch (Exception $e) { + Http::remove_cookie(Config::cookie_name); + return null; + } + } + + public function token() { + $data = [ + 'iat' => time(), + 'jti' => base64_encode(random_bytes(32)), + 'iss' => Config::server_name, + 'nbf' => time(), + 'exp' => time() + (7 * 24 * 60 * 60), + 'data' => [ + 'userid' => $this->id, + 'username' => $this->username, + 'access' => $this->access, + ], + ]; + + $key = base64_encode(Config::secret_key); + + return JWT::encode($data, $key, 'HS512'); + } +} +?> diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9af63b0 --- /dev/null +++ b/composer.json @@ -0,0 +1,5 @@ +{ + "require": { + "firebase/php-jwt": "^4.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..3dc6e22 --- /dev/null +++ b/composer.lock @@ -0,0 +1,61 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" + ], + "content-hash": "97857c969035c5b9de48fb918b6ceca2", + "packages": [ + { + "name": "firebase/php-jwt", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/firebase/php-jwt.git", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/firebase/php-jwt/zipball/dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "reference": "dccf163dc8ed7ed6a00afc06c51ee5186a428d35", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Firebase\\JWT\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Neuman Vong", + "email": "neuman+pear@twilio.com", + "role": "Developer" + }, + { + "name": "Anant Narayanan", + "email": "anant@php.net", + "role": "Developer" + } + ], + "description": "A simple library to encode and decode JSON Web Tokens (JWT) in PHP. Should conform to the current spec.", + "homepage": "https://github.com/firebase/php-jwt", + "time": "2016-07-18T04:51:16+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/public/account.php b/public/account.php new file mode 100644 index 0000000..5da3ac2 --- /dev/null +++ b/public/account.php @@ -0,0 +1,14 @@ + diff --git a/public/admin.php b/public/admin.php new file mode 100644 index 0000000..23229dd --- /dev/null +++ b/public/admin.php @@ -0,0 +1,73 @@ +access != 'admin') { + Http::redirect('index.php'); +} + +$users = User::all(); + +include 'template/header.html'; +include 'template/user_menu_button.php'; +?> + +
+
+
+
+

Users

+
+
+ +
+
+ perm_identity + username ?> +
+
+ verified_user + Access level: access ?> +
+ +
+
+ +
+
+
+ +
+
+ +
+ + add + +
+ + diff --git a/public/change_password.php b/public/change_password.php new file mode 100644 index 0000000..5df27b2 --- /dev/null +++ b/public/change_password.php @@ -0,0 +1,21 @@ + $data->username, 'password' => $_POST['password']]; +$user = UserController::authenticate($info); + +if (!$user) { + Http::redirect('account.php', ['error' => '1']); +} + +$user->password = password_hash($_POST['new_password'], PASSWORD_DEFAULT); +$user->save(); + +Http::redirect('index.php'); diff --git a/public/create.php b/public/create.php new file mode 100644 index 0000000..bf30f96 --- /dev/null +++ b/public/create.php @@ -0,0 +1,20 @@ +access != 'admin') { + Http::redirect('index.php'); +} + +/* TODO: Validate input */ +$params = Http::post_params(); +$user = new User; +$user->username = $params['username']; +$user->password = password_hash($params['password'], PASSWORD_DEFAULT); +$user->access = $params['access']; +$user->save(); + +Http::redirect('admin.php'); +?> diff --git a/public/css/admin.css b/public/css/admin.css new file mode 100644 index 0000000..3b7ee42 --- /dev/null +++ b/public/css/admin.css @@ -0,0 +1,15 @@ +#create-modal { + background: white; +} + +span.username { + padding-left: 10px; +} + +.admin-container { + padding-top: 50px; +} + +.edit-modal { + position: absolute; +} diff --git a/public/css/app.css b/public/css/app.css new file mode 100644 index 0000000..6bc964c --- /dev/null +++ b/public/css/app.css @@ -0,0 +1,17 @@ +@import url("login.css"); +@import url("admin.css"); +@import url("user.css"); + + +body { + background-color: #fcfcfc !important; +} + +main { + height: 100vh; + width: 100% !important; +} + +input:-webkit-autofill { + -webkit-box-shadow: 0 0 0 1000px white inset !important; +} diff --git a/public/css/login.css b/public/css/login.css new file mode 100644 index 0000000..193d8b4 --- /dev/null +++ b/public/css/login.css @@ -0,0 +1,13 @@ +.login-header { + padding: 20px; +} + +.login-container { + height: 100%; + margin: 0 !important; +} + +.error-message { + color: red; + margin: 15px 0 0; +} diff --git a/public/css/user.css b/public/css/user.css new file mode 100644 index 0000000..b738705 --- /dev/null +++ b/public/css/user.css @@ -0,0 +1,9 @@ +.user-button { + position: absolute; + top: 5px !important; + right: 10px !important; +} + +.user-container { + padding-top: 50px; +} diff --git a/public/delete.php b/public/delete.php new file mode 100644 index 0000000..bf67e8c --- /dev/null +++ b/public/delete.php @@ -0,0 +1,19 @@ +access != 'admin') { + Http::redirect('index.php'); +} + +$username = Http::post_params()['username']; +$user = User::get($username); + +if ($user) { + $user->delete(); +} + +Http::redirect('admin.php'); +?> diff --git a/public/edit.php b/public/edit.php new file mode 100644 index 0000000..bf30f96 --- /dev/null +++ b/public/edit.php @@ -0,0 +1,20 @@ +access != 'admin') { + Http::redirect('index.php'); +} + +/* TODO: Validate input */ +$params = Http::post_params(); +$user = new User; +$user->username = $params['username']; +$user->password = password_hash($params['password'], PASSWORD_DEFAULT); +$user->access = $params['access']; +$user->save(); + +Http::redirect('admin.php'); +?> diff --git a/public/img/background.png b/public/img/background.png new file mode 100644 index 0000000..bc382e0 Binary files /dev/null and b/public/img/background.png differ diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..ca75a68 --- /dev/null +++ b/public/index.php @@ -0,0 +1,18 @@ +access == 'user') { + Http::redirect('user.php'); + } else if ($data->access == 'admin') { + Http::redirect('admin.php'); + } +} + +include 'template/header.html'; +include(APP_DIR . 'forms/login.php'); +include 'template/footer.html'; +?> diff --git a/public/js/app.js b/public/js/app.js new file mode 100644 index 0000000..50f51c0 --- /dev/null +++ b/public/js/app.js @@ -0,0 +1,27 @@ + +/* Scale login form on page load */ +$(() => $('#login-form').addClass('scale-in')); + +/* Scale user tiles on page load */ +$(() => $('.user-tile').addClass('scale-in')); + +$(document).ready(function(){ + // the "href" attribute of .modal-trigger must specify the modal ID that wants to be triggered + $('.modal').modal(); +}); + +$('.dropdown-button').dropdown({ + inDuration: 300, + outDuration: 225, + constrainWidth: false, // Does not change width of dropdown to that of the activator + hover: true, // Activate on hover + gutter: 0, // Spacing from edge + belowOrigin: true, // Displays dropdown below the button + alignment: 'right', // Displays dropdown with edge aligned to the left of button + stopPropagation: false // Stops event propagation + } +); + +$(document).ready(function(){ + $('.collapsible').collapsible(); + }); diff --git a/public/login.php b/public/login.php new file mode 100644 index 0000000..d7e39f7 --- /dev/null +++ b/public/login.php @@ -0,0 +1,25 @@ +login_attempts; + +if ($attempts >= 8) { + Http::redirect('index.php', ['error' => '2']); +} + +$user = UserController::authenticate(Http::post_params()); + +if (!$user) { + Http::redirect('index.php', ['error' => '1']); +} + +setcookie(Config::cookie_name, $user->token()); +if ($user->access == 'user') { + Http::redirect('user.php'); +} else if ($user->access == 'admin') { + Http::redirect('admin.php'); +} + +?> diff --git a/public/logout.php b/public/logout.php new file mode 100644 index 0000000..35320c8 --- /dev/null +++ b/public/logout.php @@ -0,0 +1,8 @@ + diff --git a/public/template/footer.html b/public/template/footer.html new file mode 100644 index 0000000..6924fd7 --- /dev/null +++ b/public/template/footer.html @@ -0,0 +1,9 @@ + + + + + + diff --git a/public/template/header.html b/public/template/header.html new file mode 100644 index 0000000..3e4448c --- /dev/null +++ b/public/template/header.html @@ -0,0 +1,11 @@ + + + + Compsec Project + + + + + + +
diff --git a/public/template/user_menu_button.php b/public/template/user_menu_button.php new file mode 100644 index 0000000..5ab5ea8 --- /dev/null +++ b/public/template/user_menu_button.php @@ -0,0 +1,25 @@ + + +
+ + + username ?> + + + + +
diff --git a/public/user.php b/public/user.php new file mode 100644 index 0000000..313b763 --- /dev/null +++ b/public/user.php @@ -0,0 +1,65 @@ + + +
+
+

Welcome, username ?>!


+
Here's a bit of interesting information about this site...

+
    +
  • +
    + lock_outline + User authentication +
    +
    + + Users are authenticated with this website using JWT + tokens which are generated server-side. Upon signing in, a user is given a unique + token containing non-sensitive user information which is stored client-side as a + browser cookie. + +
    +
  • +
  • +
    + vpn_key + Password hashing +
    +
    + + Passwords are salted and hashed server-side and stored in the + user database. Passwords are hashed using PHP's bcrypt algorithm. + +
    +
  • +
  • +
    + https + HTTPS support +
    +
    + + This website uses HTTPS (HTTP over SSL) to communicate between client + and server. All HTTP requests and responses are securely encrypted before + being sent. HTTPS makes use of public-key cryptography. + +
    +
  • +
+
+
+ +