From ac7f6f5b5644687882002d19b4d1a158bf4a1801 Mon Sep 17 00:00:00 2001 From: Lukas Metzger Date: Tue, 20 Mar 2018 10:51:47 +0100 Subject: [PATCH] Created framwork for backend, working authentication and session management --- .gitignore | 3 + backend/composer.json | 16 + backend/composer.lock | 440 ++++++++++++++++++ backend/config/ConfigDefault.php | 30 ++ backend/controllers/NotAllowed.php | 18 + backend/controllers/NotFound.php | 16 + backend/controllers/Sessions.php | 78 ++++ .../exceptions/PluginNotFoundException.php | 9 + backend/middlewares/Authentication.php | 44 ++ backend/middlewares/LogRequests.php | 26 ++ backend/middlewares/RejectEmptyBody.php | 31 ++ backend/operations/Sessionstorage.php | 66 +++ backend/operations/UserAuth.php | 151 ++++++ .../InterfaceSessionstorage.php | 50 ++ backend/plugins/Sessionstorage/apcu.php | 90 ++++ .../plugins/UserAuth/InterfaceUserAuth.php | 31 ++ backend/plugins/UserAuth/native.php | 54 +++ backend/public/index.php | 39 ++ backend/services/Database.php | 37 ++ backend/services/Logger.php | 28 ++ 20 files changed, 1257 insertions(+) create mode 100644 backend/composer.json create mode 100644 backend/composer.lock create mode 100644 backend/config/ConfigDefault.php create mode 100644 backend/controllers/NotAllowed.php create mode 100644 backend/controllers/NotFound.php create mode 100644 backend/controllers/Sessions.php create mode 100644 backend/exceptions/PluginNotFoundException.php create mode 100644 backend/middlewares/Authentication.php create mode 100644 backend/middlewares/LogRequests.php create mode 100644 backend/middlewares/RejectEmptyBody.php create mode 100644 backend/operations/Sessionstorage.php create mode 100644 backend/operations/UserAuth.php create mode 100644 backend/plugins/Sessionstorage/InterfaceSessionstorage.php create mode 100644 backend/plugins/Sessionstorage/apcu.php create mode 100644 backend/plugins/UserAuth/InterfaceUserAuth.php create mode 100644 backend/plugins/UserAuth/native.php create mode 100644 backend/public/index.php create mode 100644 backend/services/Database.php create mode 100644 backend/services/Logger.php diff --git a/.gitignore b/.gitignore index 40e8502..5af45fa 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ backend-legacy/config/config-user.php */nbproject/private */node_modules/* + +backend/vendor/* +backend/config/ConfigUser.php diff --git a/backend/composer.json b/backend/composer.json new file mode 100644 index 0000000..563e157 --- /dev/null +++ b/backend/composer.json @@ -0,0 +1,16 @@ +{ + "require": { + "slim/slim": "^3.9", + "monolog/monolog": "^1.23" + }, + "autoload": { + "psr-4": { + "Services\\": "services/", + "Controllers\\": "controllers/", + "Operations\\": "operations/", + "Plugins\\": "plugins/", + "Middlewares\\": "middlewares/", + "Exceptions\\": "exceptions/" + } + } +} \ No newline at end of file diff --git a/backend/composer.lock b/backend/composer.lock new file mode 100644 index 0000000..74a466a --- /dev/null +++ b/backend/composer.lock @@ -0,0 +1,440 @@ +{ + "_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": "cacaa59ea8f8c157d77bb7e3a8d52295", + "packages": [ + { + "name": "container-interop/container-interop", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/container-interop/container-interop.git", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/container-interop/container-interop/zipball/79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "reference": "79cbf1341c22ec75643d841642dd5d6acd83bdb8", + "shasum": "" + }, + "require": { + "psr/container": "^1.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Interop\\Container\\": "src/Interop/Container/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Promoting the interoperability of container objects (DIC, SL, etc.)", + "homepage": "https://github.com/container-interop/container-interop", + "time": "2017-02-14T19:40:03+00:00" + }, + { + "name": "monolog/monolog", + "version": "1.23.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "reference": "fd8c787753b3a2ad11bc60c063cff1358a32a3b4", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "provide": { + "psr/log-implementation": "1.0.0" + }, + "require-dev": { + "aws/aws-sdk-php": "^2.4.9 || ^3.0", + "doctrine/couchdb": "~1.0@dev", + "graylog2/gelf-php": "~1.0", + "jakub-onderka/php-parallel-lint": "0.9", + "php-amqplib/php-amqplib": "~2.4", + "php-console/php-console": "^3.1.3", + "phpunit/phpunit": "~4.5", + "phpunit/phpunit-mock-objects": "2.3.0", + "ruflin/elastica": ">=0.90 <3.0", + "sentry/sentry": "^0.13", + "swiftmailer/swiftmailer": "^5.3|^6.0" + }, + "suggest": { + "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB", + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server", + "mongodb/mongodb": "Allow sending log messages to a MongoDB server via PHP Driver", + "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", + "php-console/php-console": "Allow sending log messages to Google Chrome", + "rollbar/rollbar": "Allow sending log messages to Rollbar", + "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "sentry/sentry": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Monolog\\": "src/Monolog" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2017-06-19T01:22:40+00:00" + }, + { + "name": "nikic/fast-route", + "version": "v1.3.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/FastRoute.git", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/FastRoute/zipball/181d480e08d9476e61381e04a71b34dc0432e812", + "reference": "181d480e08d9476e61381e04a71b34dc0432e812", + "shasum": "" + }, + "require": { + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.8.35|~5.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "FastRoute\\": "src/" + }, + "files": [ + "src/functions.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov", + "email": "nikic@php.net" + } + ], + "description": "Fast request router for PHP", + "keywords": [ + "router", + "routing" + ], + "time": "2018-02-13T20:26:39+00:00" + }, + { + "name": "pimple/pimple", + "version": "v3.2.3", + "source": { + "type": "git", + "url": "https://github.com/silexphp/Pimple.git", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/silexphp/Pimple/zipball/9e403941ef9d65d20cba7d54e29fe906db42cf32", + "reference": "9e403941ef9d65d20cba7d54e29fe906db42cf32", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/container": "^1.0" + }, + "require-dev": { + "symfony/phpunit-bridge": "^3.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.2.x-dev" + } + }, + "autoload": { + "psr-0": { + "Pimple": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + } + ], + "description": "Pimple, a simple Dependency Injection Container", + "homepage": "http://pimple.sensiolabs.org", + "keywords": [ + "container", + "dependency injection" + ], + "time": "2018-01-21T07:42:36+00:00" + }, + { + "name": "psr/container", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/container.git", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/container/zipball/b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "reference": "b7ce3b176482dbbc1245ebf52b181af44c2cf55f", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Container\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common Container Interface (PHP FIG PSR-11)", + "homepage": "https://github.com/php-fig/container", + "keywords": [ + "PSR-11", + "container", + "container-interface", + "container-interop", + "psr" + ], + "time": "2017-02-14T16:28:37+00:00" + }, + { + "name": "psr/http-message", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/http-message.git", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/http-message/zipball/f6561bf28d520154e4b0ec72be95418abe6d9363", + "reference": "f6561bf28d520154e4b0ec72be95418abe6d9363", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Http\\Message\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for HTTP messages", + "homepage": "https://github.com/php-fig/http-message", + "keywords": [ + "http", + "http-message", + "psr", + "psr-7", + "request", + "response" + ], + "time": "2016-08-06T14:39:51+00:00" + }, + { + "name": "psr/log", + "version": "1.0.2", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "reference": "4ebe3a8bf773a19edfe0a84b6585ba3d401b724d", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2016-10-10T12:19:37+00:00" + }, + { + "name": "slim/slim", + "version": "3.9.2", + "source": { + "type": "git", + "url": "https://github.com/slimphp/Slim.git", + "reference": "4086d0106cf5a7135c69fce4161fe355a8feb118" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/slimphp/Slim/zipball/4086d0106cf5a7135c69fce4161fe355a8feb118", + "reference": "4086d0106cf5a7135c69fce4161fe355a8feb118", + "shasum": "" + }, + "require": { + "container-interop/container-interop": "^1.2", + "nikic/fast-route": "^1.0", + "php": ">=5.5.0", + "pimple/pimple": "^3.0", + "psr/container": "^1.0", + "psr/http-message": "^1.0" + }, + "provide": { + "psr/http-message-implementation": "1.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Slim\\": "Slim" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Rob Allen", + "email": "rob@akrabat.com", + "homepage": "http://akrabat.com" + }, + { + "name": "Josh Lockhart", + "email": "hello@joshlockhart.com", + "homepage": "https://joshlockhart.com" + }, + { + "name": "Gabriel Manricks", + "email": "gmanricks@me.com", + "homepage": "http://gabrielmanricks.com" + }, + { + "name": "Andrew Smith", + "email": "a.smith@silentworks.co.uk", + "homepage": "http://silentworks.co.uk" + } + ], + "description": "Slim is a PHP micro framework that helps you quickly write simple yet powerful web applications and APIs", + "homepage": "https://slimframework.com", + "keywords": [ + "api", + "framework", + "micro", + "router" + ], + "time": "2017-11-26T19:13:09+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} diff --git a/backend/config/ConfigDefault.php b/backend/config/ConfigDefault.php new file mode 100644 index 0000000..6cb9ec6 --- /dev/null +++ b/backend/config/ConfigDefault.php @@ -0,0 +1,30 @@ + [ + 'host' => 'localhost', + 'user' => 'user', + 'password' => 'password', + 'dbname' => 'pdnsmanager', + 'port' => 3306 + ], + 'logging' => [ + 'level' => 'info', + 'path' => '' + ], + 'sessionstorage' => [ + 'plugin' => 'apcu', + 'timeout' => 3600, + 'config' => null + ], + 'authentication' => [ + 'default' => [ + 'plugin' => 'native', + 'config' => null + ] + ] +]; + +$userConfig = require('ConfigUser.php'); + +return array('config' => array_replace_recursive($defaultConfig, $userConfig)); diff --git a/backend/controllers/NotAllowed.php b/backend/controllers/NotAllowed.php new file mode 100644 index 0000000..9372a08 --- /dev/null +++ b/backend/controllers/NotAllowed.php @@ -0,0 +1,18 @@ +logger->warning('Method ' . $request->getMethod() . ' is not valid for ' . $request->getUri()->getPath()); + return $c['response'] + ->withHeader('Allow', \implode(', ', $methods)) + ->withJson(array('error' => 'Method ' . $request->getMethod() . ' is not valid use on of ' . implode(', ', $methods)), 405); + }; + } +} diff --git a/backend/controllers/NotFound.php b/backend/controllers/NotFound.php new file mode 100644 index 0000000..6d431a0 --- /dev/null +++ b/backend/controllers/NotFound.php @@ -0,0 +1,16 @@ +logger->warning('No valid endpoint found for: ' . $request->getUri()->getPath()); + return $c['response']->withJson(array('error' => 'No valid endpoint found!'), 404); + }; + } +} diff --git a/backend/controllers/Sessions.php b/backend/controllers/Sessions.php new file mode 100644 index 0000000..e1b042d --- /dev/null +++ b/backend/controllers/Sessions.php @@ -0,0 +1,78 @@ +logger = $c->logger; + $this->container = $c; + } + + public function post(Request $req, Response $res, array $args) + { + $body = $req->getParsedBody(); + + if (!array_key_exists('username', $body) || + !array_key_exists('password', $body)) { + return $res->withJson(['error' => 'One of the required fields is missing'], 422); + } + + $userAuth = new \Operations\UserAuth($this->container); + $sessionStorage = new \Operations\Sessionstorage($this->container); + + $sessionTimeout = $this->container['config']['sessionstorage']['timeout']; + + try { + $userId = $userAuth->authenticate($body['username'], $body['password']); + } catch (\Exceptions\PluginNotFoundException $e) { + return $res->withJson(['error' => $e->getMessage()], 500); + } + + if ($userId >= 0) { + $secret = openssl_random_pseudo_bytes(64); + $secretString = base64_encode($secret); + $secretString = rtrim(strtr($secretString, '+/', '-_'), '='); + + $sessionStorage->set($secretString, $userId, $sessionTimeout); + + $this->logger->info('User authenticated successfully', ['username' => $body['username']]); + return $res->withJson([ + 'username' => $body['username'], + 'token' => $secretString + ], 201); + } else { + $this->logger->info('User failed to authenticate', ['username' => $body['username']]); + return $res->withJson(['error' => 'Username or password is invalid'], 403); + } + } + + public function delete(Request $req, Response $res, array $args) + { + $sessionStorage = new \Operations\Sessionstorage($this->container); + + if ($sessionStorage->exists($args['sessionId'])) { + $sessionStorage->delete($args['sessionId']); + + $this->logger->info('Deleting session', ['token' => $args['sessionId']]); + return $res->withStatus(204); + } else { + $this->logger->warning('Trying to delete non existing session', ['token' => $args['sessionId']]); + return $res->withJson(['error' => 'Session not found'], 404); + } + + + } +} diff --git a/backend/exceptions/PluginNotFoundException.php b/backend/exceptions/PluginNotFoundException.php new file mode 100644 index 0000000..61ccb22 --- /dev/null +++ b/backend/exceptions/PluginNotFoundException.php @@ -0,0 +1,9 @@ +logger = $c->logger; + $this->container = $c; + } + + public function __invoke(Request $req, Response $res, callable $next) + { + $token = $req->getHeaderLine('X-Authentication'); + + $sessionStorage = new \Operations\Sessionstorage($this->container); + + if ($sessionStorage->exists($token)) { + $sessionTimeout = $this->container['config']['sessionstorage']['timeout']; + + $userId = $sessionStorage->get($token, $sessionTimeout); + + $this->logger->debug('Authentication was successfull', ['token' => $token, 'userId' => $userId]); + + $req = $req->withAttribute('userId', $userId); + return $next($req, $res); + } else { + $this->logger->warning('No valid authentication token found'); + return $res->withJson(['error' => 'No valid authentication token suplied'], 403); + } + } +} diff --git a/backend/middlewares/LogRequests.php b/backend/middlewares/LogRequests.php new file mode 100644 index 0000000..e2a7dc5 --- /dev/null +++ b/backend/middlewares/LogRequests.php @@ -0,0 +1,26 @@ +logger = $c->logger; + } + + public function __invoke(Request $req, Response $res, callable $next) + { + $this->logger->debug($req->getMethod() . ' ' . $req->getUri()->getPath()); + + return $next($req, $res); + } +} diff --git a/backend/middlewares/RejectEmptyBody.php b/backend/middlewares/RejectEmptyBody.php new file mode 100644 index 0000000..f6a304d --- /dev/null +++ b/backend/middlewares/RejectEmptyBody.php @@ -0,0 +1,31 @@ +logger = $c->logger; + } + + public function __invoke(Request $req, Response $res, callable $next) + { + if (($req->isPost() || $req->isPut() || $req->isPatch()) && $req->getParsedBody() == null) { + $this->logger->warning('Got empty body in request with method ' . $req->getMethod()); + + return $res->withJson(['error' => 'The supplied body was empty'], 400); + } else { + return $next($req, $res); + } + + } +} diff --git a/backend/operations/Sessionstorage.php b/backend/operations/Sessionstorage.php new file mode 100644 index 0000000..e0167a1 --- /dev/null +++ b/backend/operations/Sessionstorage.php @@ -0,0 +1,66 @@ +logger = $c->logger; + + $config = $c['config']['sessionstorage']; + + $plugin = $config['plugin']; + $pluginConfig = $config['config']; + + $pluginClass = '\\Plugins\\Sessionstorage\\' . $plugin; + + //Check if plugin is available + if (!class_exists($pluginClass)) { + $this->logger->critical('The configured session storage plugin does not exist', ['plugin' => $plugin]); + exit(); + } + + //Try to create class with given name + $this->backend = new $pluginClass($this->logger, $pluginConfig); + + if (!$this->backend instanceof \Plugins\Sessionstorage\InterfaceSessionstorage) { + $this->logger->critical('The configured plugin does not implement InterfaceSessionstorage', ['pluginname' => $plugin]); + exit(); + } + + $this->logger->debug("Session storage plugin was loaded", ['plugin' => $plugin]); + } + + public function set(string $key, string $value, int $ttl) : void + { + $this->backend->set($key, $value, $ttl); + } + + public function exists(string $key) : bool + { + return $this->backend->exists($key); + } + + public function get(string $key, int $ttl) : string + { + return $this->backend->get($key, $ttl); + } + + public function delete(string $key) : void + { + $this->backend->delete($key); + } +} \ No newline at end of file diff --git a/backend/operations/UserAuth.php b/backend/operations/UserAuth.php new file mode 100644 index 0000000..518a733 --- /dev/null +++ b/backend/operations/UserAuth.php @@ -0,0 +1,151 @@ +logger = $c->logger; + $this->db = $c->db; + $this->c = $c; + } + + /** + * Authenticates a user with username/password combination. + * + * @param $username Username + * @param $password Password + * + * @return int -1 if authentication failed, the user id otherwise + * + * @throws \Exceptions\PluginNotFoundExecption if no matching backend can be found + */ + public function authenticate(string $username, string $password) : int + { + if (strpos($username, '/') === false) { // no explicit backend specification + $backend = 'default'; + $name = $username; + } else { + $parts = preg_split('/\//', $username, 2); + $backend = $parts[0]; + $name = $parts[1]; + } + + $this->logger->debug('Trying to authenticate with info', ['backend' => $backend, 'name' => $name]); + + try { + if ($this->authenticateBackend($backend, $name, $password)) { + return $this->localUser($backend, $name, $password); + } else { + return -1; + } + } catch (\Exceptions\PluginNotFoundException $e) { + throw $e; + } + + } + + /** + * This function searches for an apropriate backend and calls it + * to authenticate the user. + * + * @param $backend The name of the backend to use + * @param $username The username to use + * @param $password The password to use + * + * @return bool true if authentication successfull false otherwise + * + * @throws \Exceptions\PluginNotFoundExecption if no matching backend can be found + */ + private function authenticateBackend(string $backend, string $username, string $password) : bool + { + $config = $this->c['config']['authentication']; + + if (!array_key_exists($backend, $config)) { // Check if backend is configured for prefix + $this->logger->warning('No authentication backend configured for prefix', ['prefix' => $backend]); + throw new PluginNotFoundException('No authentication backend configured for this user.'); + } + + $plugin = $config[$backend]['plugin']; + $pluginClass = '\\Plugins\\UserAuth\\' . $plugin; + $pluginConfig = $config[$backend]['config']; + + if (!class_exists($pluginClass)) { // Check if given backend class exists + $this->logger->error('The configured UserAuth plugin does not exist', ['prefix' => $backend, 'plugin' => $plugin]); + throw new PluginNotFoundException('The authentication request can not be processed.'); + } + + //Try to create class with given name + $backendObj = new $pluginClass($this->logger, $this->db, $pluginConfig); + + if (!$backendObj instanceof \Plugins\UserAuth\InterfaceUserAuth) { // Check if class implements interface + $this->logger->error('The configured plugin does not implement InterfaceUserAuth', ['plugin' => $plugin, 'prefix' => $backend]); + throw new PluginNotFoundException('The authentication request can not be processed.'); + } + + $this->logger->debug("UserAuth plugin was loaded", ['plugin' => $plugin, 'prefix' => $backend]); + + return $backendObj->authenticate($username, $password); + } + + /** + * Ensures the user from the given backend has a entry in the local database, + * then returns the user id. + * + * @param $backend The name of the backend to use + * @param $username The username to use + * @param $password The password to use + * + * @return int The local user id + */ + private function localUser(string $backend, string $username, string $password) : int + { + $this->db->beginTransaction(); + + $query = $this->db->prepare('SELECT id FROM users WHERE name=:name AND backend=:backend'); + $query->bindValue(':name', $username, \PDO::PARAM_STR); + $query->bindValue(':backend', $backend); + $query->execute(); + + $record = $query->fetch(); + + if ($record === false) { + $insert = $this->db->prepare('INSERT INTO users (name,backend,type) VALUES (:name, :backend, \'user\')'); + $insert->bindValue(':name', $username, \PDO::PARAM_STR); + $insert->bindValue(':backend', $backend, \PDO::PARAM_STR); + $insert->execute(); + + $query->execute(); + + $record = $query->fetch(); + + $this->logger->info('Non existing user created', ['username' => $username, 'backend' => $backend, 'newId' => $record['id']]); + } else { + $this->logger->debug('User was found in database', ['username' => $username, 'backend' => $backend, 'id' => $record['id']]); + } + + $this->db->commit(); + + return $record['id']; + } +} \ No newline at end of file diff --git a/backend/plugins/Sessionstorage/InterfaceSessionstorage.php b/backend/plugins/Sessionstorage/InterfaceSessionstorage.php new file mode 100644 index 0000000..4722764 --- /dev/null +++ b/backend/plugins/Sessionstorage/InterfaceSessionstorage.php @@ -0,0 +1,50 @@ +logger = $logger; + + if (!function_exists('apcu_store')) { + $this->$logger->critical('PHP APCu extension is not available but configured as session storage backend exiting now'); + exit(); + } + } + + /** + * Save new entry. + * + * @param $key The key for the entry + * @param $value The value for the entry + * @param $ttl The time (in s) for which this item should be available + */ + public function set(string $key, string $value, int $ttl) : void + { + $this->logger->debug('Storing data to APCu', ['key' => $key, 'value' => $value, 'ttl' => $ttl]); + + apcu_store($key, $value, $ttl); + } + + /** + * Queries the existence of some entry. + * + * @param $key The key to query + */ + public function exists(string $key) : bool + { + $this->logger->debug('Checking for APCu key existence', ['key' => $key]); + + return apcu_exists($key); + } + + /** + * Get the value for a given key. This should also reset the ttl to the given value. + * + * @param $key The key for the entry to get + * @param $ttl The new ttl for the entry + */ + public function get(string $key, int $ttl) : string + { + $this->logger->debug('Getting data from APCu', ['key' => $key, 'ttl' => $ttl]); + + $value = apcu_fetch($key); + + if ($value == false) { + $this->logger->error('Non existing key was queried from APCu', ['key' => $key]); + throw new \InvalidArgumentException('The requested key was not in the database!'); + } + + apcu_store($key, $value, $ttl); + + return $value; + } + + /** + * Delete the value for a given key. + * + * @param $key The key to delete + */ + public function delete(string $key) : void + { + $this->logger->debug('Deleting key from APCu', ['key' => $key]); + + apcu_delete($key); + } +} \ No newline at end of file diff --git a/backend/plugins/UserAuth/InterfaceUserAuth.php b/backend/plugins/UserAuth/InterfaceUserAuth.php new file mode 100644 index 0000000..7fbf581 --- /dev/null +++ b/backend/plugins/UserAuth/InterfaceUserAuth.php @@ -0,0 +1,31 @@ +logger = $logger; + $this->db = $db; + } + + /** + * Authenticate user. + * + * @param $username The key for the entry + * @param $password The value for the entry + * + * @return true if valid false otherwise + */ + public function authenticate(string $username, string $password) : bool + { + $query = $this->db->prepare('SELECT id, password FROM users WHERE name=:name AND backend=\'native\''); + $query->bindValue(':name', $username, \PDO::PARAM_STR); + $query->execute(); + + $record = $query->fetch(); + + if ($record === false) { + return false; + } + + return password_verify($password, $record['password']); + } +} \ No newline at end of file diff --git a/backend/public/index.php b/backend/public/index.php new file mode 100644 index 0000000..c5a4ffe --- /dev/null +++ b/backend/public/index.php @@ -0,0 +1,39 @@ +group('/v1', function () { + $this->post('/sessions', '\Controllers\Sessions:post'); + + $this->group('', function () { + $this->delete('/sessions/{sessionId}', '\Controllers\Sessions:delete'); + })->add('\Middlewares\Authentication'); + +}); + +// Add global middlewares +$app->add('\Middlewares\LogRequests'); +$app->add('\Middlewares\RejectEmptyBody'); + +// Run application +$app->run(); + diff --git a/backend/services/Database.php b/backend/services/Database.php new file mode 100644 index 0000000..0e868f8 --- /dev/null +++ b/backend/services/Database.php @@ -0,0 +1,37 @@ +logger->critical("SQL Connect Error: " . $e->getMessage()); + $c->logger->critical("DB Config was", $config); + exit(); + } + + try { + $pdo->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION); + $pdo->setAttribute(\PDO::ATTR_DEFAULT_FETCH_MODE, \PDO::FETCH_ASSOC); + } catch (\PDOException $e) { + $c->logger->critical("SQL Parameter Error: " . $e->getMessage()); + exit(); + } + + $c->logger->debug("Database setup successfull"); + + return $pdo; + } +} diff --git a/backend/services/Logger.php b/backend/services/Logger.php new file mode 100644 index 0000000..263479d --- /dev/null +++ b/backend/services/Logger.php @@ -0,0 +1,28 @@ + 0) { + $fileHandler = new \Monolog\Handler\StreamHandler($path, $loglevel); + $logger->pushHandler($fileHandler); + } else { + $errorLogHandler = new \Monolog\Handler\ErrorLogHandler(0, $loglevel); + $logger->pushHandler($errorLogHandler); + } + + return $logger; + } +}