diff --git a/README.md b/README.md index 2c9ab3f..481cb5a 100644 --- a/README.md +++ b/README.md @@ -9,24 +9,12 @@ Simple security library for web applications. composer require kavalanche/security ``` -2. Create `UserProvider`. +2. Create `UserProvider`. Refer to [Custom UserProvider](#custom-userprovider) section. + +3. Instantiate `AuthenticationProvider` and inject your `UserProvider` into it. ```php - // $users must be an array of objects implementing Kavalanche\Security\UserInterface - $userProvider = new Kavalanche\Security\InMemoryUserProvider($users); - ``` - - OR - - ```php - // $db is a databse handle (for exapmple PDO instance) - $userProvider = new Kavalanche\Security\DatabaseUserProvider($db); - ``` - -3. Create `AuthenticationProvider` and inject your UserProvider into it. - - ```php - $authenticationProvider = new Kavalanche\Security\AuthenticationProvider($userProvider); + $authenticationProvider = new Kavalanche\Security\Provider\AuthenticationProvider($userProvider); ``` 4. Check if user is authenticated. @@ -34,33 +22,33 @@ Simple security library for web applications. ```php try { $user = $authenticationProvider->authenticate(); - } catch (Exception $ex) { + } catch (Kavalanche\Security\Exception\SecurityException $ex) { // if you want to allow unauthenticated users, then assign false or null to $user // if you require user to be authenticated do as follows - if (!$e instanceof Kavalanche\Security\UserNotAuthenticatedException) { + if (!$e instanceof Kavalanche\Security\Exception\UserNotAuthenticatedException) { // put message in flash session and redirect to user form // or do whatever your use case demands } } ``` - You can specify `redirect_path` in `security.yaml`. Default is `/`. + You can specify `redirect-path` in `security.yaml`. Default is `/`. By default expected login form field names are `email` and `password`. You can change them by creating a config file named `{app_root}/config/security.yaml` and setting these variables: - - `login_form_identifier_field` for identifier field - - `login_form_password_field` for password field + - `login-form-identifier-field` for identifier field + - `login-form-password-field` for password field 5. Don't forget to put `session_start()` at the beginning of your file. -# Custom `UserProvider` +# Custom UserProvider -If you want to user different UserProvider, you can create your. It must implement `Kavalanche\Security\UserProviderInterface`. +If you want to user different UserProvider, you can create your. It must implement `Kavalanche\Security\Interface\UserProviderInterface`. ```php -class UserProvider implements UserProviderInterface { +class UserProvider implements Kavalanche\Security\Interface\UserProviderInterface { public function loadUser($identifier) { @@ -72,7 +60,7 @@ class UserProvider implements UserProviderInterface { return $user; } - throw new Kavalanche\Security\SecurityException('Invalid username.'); + throw new Kavalanche\Security\Exception\SecurityException('Invalid username.'); } } @@ -85,12 +73,9 @@ You can configure identifier type in your `{app_root}/config/security.yaml` file - It's your obligation to ensure that the usernames are unique. - You can use this library however you want. To secure the whole application or just some routes. -- There is a demonstration of use in the public directory of this library. Go ahead and test it. - You can add multiple roles to each user. Simply assign an array with roles or permissions with `$user->setRoles()` setter. -- If you want to check that the user has appropriate roles (is authorized), you have to pass $user variable to your checking mechanism. # To do - Implement some kind of request abstraction to encapsulate Requests (symfony/http-foundation?) - Add helper to check permissions -- Translate exception messages \ No newline at end of file diff --git a/config/config.php b/config/config.php index 7156fad..b30a69c 100644 --- a/config/config.php +++ b/config/config.php @@ -2,7 +2,7 @@ namespace Kavalanche\Security; -function getValueFromConfig(string $key) { +function config(string $key) { $config = yaml_parse_file(__DIR__ . '/parameters.yaml'); $userConfigPath = __DIR__ . '/../../../../config/security.yaml'; if (file_exists($userConfigPath)) { diff --git a/config/parameters.yaml b/config/parameters.yaml index 79db885..edf6d57 100644 --- a/config/parameters.yaml +++ b/config/parameters.yaml @@ -1,4 +1,5 @@ identifier: "email" -login_form_identifier_field: "email" -login_form_password_field: "password" -redirect_path: "/" \ No newline at end of file +login-form-identifier-field: "email" +login-form-password-field: "password" +redirect-path: "/" +lang: en diff --git a/data/users b/data/users deleted file mode 100644 index 0cf83f7..0000000 Binary files a/data/users and /dev/null differ diff --git a/docker/nginx/kavalanche-security.conf b/docker/nginx/kavalanche-security.conf deleted file mode 100644 index 5abf417..0000000 --- a/docker/nginx/kavalanche-security.conf +++ /dev/null @@ -1,44 +0,0 @@ -server { - listen 80 default_server; -# server_name kavalanche-security.test; - root /app/public; - - location / { - # try to serve file directly, fallback to index.php - try_files $uri /index.php$is_args$args; - } - - location ~ ^/index\.php(/|$) { - fastcgi_pass php-fpm:9000; - fastcgi_split_path_info ^(.+\.php)(/.*)$; - include fastcgi_params; - - # optionally set the value of the environment variables used in the application - # fastcgi_param APP_ENV prod; - # fastcgi_param APP_SECRET ; - # fastcgi_param DATABASE_URL "mysql://db_user:db_pass@host:3306/db_name"; - - # When you are using symlinks to link the document root to the - # current version of your application, you should pass the real - # application path instead of the path to the symlink to PHP - # FPM. - # Otherwise, PHP's OPcache may not properly detect changes to - # your PHP files (see https://github.com/zendtech/ZendOptimizerPlus/issues/126 - # for more information). - fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name; - fastcgi_param DOCUMENT_ROOT $realpath_root; - # Prevents URIs that include the front controller. This will 404: - # http://domain.tld/index.php/some-path - # Remove the internal directive to allow URIs like this - internal; - } - - # return 404 for all other php files not matching the front controller - # this prevents access to other php files you don't want to be accessible. - location ~ \.php$ { - return 404; - } - - error_log /var/log/nginx/kavalanche-security_error.log; - access_log /var/log/nginx/kavalanche-security_access.log; -} diff --git a/docker/php-fpm/dockerfile b/docker/php-fpm/dockerfile deleted file mode 100644 index c00a89a..0000000 --- a/docker/php-fpm/dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM php:7.4-fpm - -RUN docker-php-ext-install pdo_mysql - -ADD https://github.com/mlocati/docker-php-extension-installer/releases/latest/download/install-php-extensions /usr/local/bin/ -RUN chmod +x /usr/local/bin/install-php-extensions && sync && \ - install-php-extensions yaml - -RUN cp $PHP_INI_DIR/php.ini-development $PHP_INI_DIR/php.ini \ No newline at end of file diff --git a/public/index.php b/public/index.php deleted file mode 100644 index d4e650a..0000000 --- a/public/index.php +++ /dev/null @@ -1,92 +0,0 @@ - 1, - 'title' => 'Secret note', - 'body' => 'Secret content' - ], - [ - 'user_id' => 2, - 'title' => 'Hello world', - 'body' => 'How are you doing?' - ] - ]; - $articles = []; - foreach ($data as $article) { - if ($article['user_id'] === $user->getId()) { - $articles[] = (object) $article; - } - } - return $articles; -} - -$users = unserialize(file_get_contents(__DIR__ . '/../data/users')); -$userProvider = new InMemoryUserProvider($users); -$authenticationProvider = new AuthenticationProvider($userProvider); - -try { - $user = $authenticationProvider->authenticate(); -} catch (SecurityException $ex) { - $user = false; -} - -if (isset($_GET['login'])) { - try { - $user = $authenticationProvider->authenticate(); - } catch (SecurityException $ex) { - if (!$ex instanceof UserNotAuthenticatedException) { - ?> -

getMessage(); ?>

- -
-
-
- -
- getName() ?? "anonymous") . ' | Log out'; -} else { - echo 'Log in'; -} -if (!empty($user)) { - $articles = getUserArticles($user); -} else { - $articles = []; -} - -foreach ($articles as $article) { - ?> -
-

title; ?>

-
body; ?>
-
- Log in to see your posts.

'; -} \ No newline at end of file diff --git a/src/AuthenticationProvider.php b/src/AuthenticationProvider.php deleted file mode 100644 index 0c75445..0000000 --- a/src/AuthenticationProvider.php +++ /dev/null @@ -1,46 +0,0 @@ - - */ -class AuthenticationProvider { - - private $userProvider; - - public function __construct(UserProviderInterface $userProvider) { - $this->userProvider = $userProvider; - } - - public function authenticate() { - if (!in_array(getValueFromConfig('identifier'), ['email', 'username'], true)) { - throw new Exception('User identifier must be one of: email, username'); - } - - if (!empty($_SESSION[getValueFromConfig('identifier')])) { - $user = $this->userProvider->loadUser($_SESSION[getValueFromConfig('identifier')]); - return $user; - } - - if (!empty($_POST[getValueFromConfig('login_form_identifier_field')])) { - $user = $this->userProvider->loadUser($_POST[getValueFromConfig('login_form_identifier_field')]); - if (password_verify($_POST[getValueFromConfig('login_form_password_field')], $user->getPassword())) { - if (getValueFromConfig('identifier') === 'email') { - $_SESSION[getValueFromConfig('identifier')] = $user->getEmail(); - } elseif (getValueFromConfig('identifier') === 'username') { - $_SESSION[getValueFromConfig('identifier')] = $user->getUsername(); - } - header('Location: ' . getValueFromConfig('redirect_path')); - exit; - } - http_response_code(401); - throw new SecurityException('Wrong data provided'); - } - http_response_code(401); - throw new UserNotAuthenticatedException('Please log in'); - } - -} diff --git a/src/DatabaseUserProvider.php b/src/DatabaseUserProvider.php deleted file mode 100644 index ac932a7..0000000 --- a/src/DatabaseUserProvider.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ -class DatabaseUserProvider implements UserProviderInterface { - - private $db; - - public function __construct(PDO $db) { - $this->db = $db; - } - - public function loadUser(string $identifier): UserInterface { - if (getValueFromConfig('identifier') === 'email') { - $sql = "SELECT * FROM user WHERE email = :identifier"; - } elseif (getValueFromConfig('identifier') === 'username') { - $sql = "SELECT * FROM user WHERE username = :identifier"; - } - $stmt = $this->db->prepare($sql); - $stmt->execute([ - 'identifier' => $identifier - ]); - $stmt->setFetchMode(PDO::FETCH_CLASS, User::class); - $user = $stmt->fetch() ?: null; - - if (!empty($user) && $user instanceof UserInterface) { - return $user; - } - - throw new SecurityException('Wrong data provided'); - } - -} diff --git a/src/SecurityException.php b/src/Exception/SecurityException.php similarity index 74% rename from src/SecurityException.php rename to src/Exception/SecurityException.php index 978c0df..7e54774 100644 --- a/src/SecurityException.php +++ b/src/Exception/SecurityException.php @@ -1,6 +1,6 @@ diff --git a/src/UserNotAuthenticatedException.php b/src/Exception/UserNotAuthenticatedException.php similarity index 76% rename from src/UserNotAuthenticatedException.php rename to src/Exception/UserNotAuthenticatedException.php index 7660372..6b4aa72 100644 --- a/src/UserNotAuthenticatedException.php +++ b/src/Exception/UserNotAuthenticatedException.php @@ -1,6 +1,6 @@ diff --git a/src/InMemoryUserProvider.php b/src/InMemoryUserProvider.php deleted file mode 100644 index 5828ff8..0000000 --- a/src/InMemoryUserProvider.php +++ /dev/null @@ -1,30 +0,0 @@ - - */ -class InMemoryUserProvider implements UserProviderInterface { - - private $users; - - public function __construct(array $users) { - $this->users = $users; - } - - public function loadUser(string$identifier): UserInterface { - foreach ($this->users as $user) { - if ( - (getValueFromConfig('identifier') === 'email' && $user->getEmail() === $identifier) || (getValueFromConfig('identifier') === 'username' && $user->getUsername() === $identifier) && $user instanceof UserInterface - ) { - return $user; - } - } - - throw new SecurityException('Wrong data provided'); - } - -} diff --git a/src/UserInterface.php b/src/Interface/UserInterface.php similarity index 100% rename from src/UserInterface.php rename to src/Interface/UserInterface.php diff --git a/src/UserProviderInterface.php b/src/Interface/UserProviderInterface.php similarity index 100% rename from src/UserProviderInterface.php rename to src/Interface/UserProviderInterface.php diff --git a/src/Provider/AuthenticationProvider.php b/src/Provider/AuthenticationProvider.php new file mode 100644 index 0000000..fc187a0 --- /dev/null +++ b/src/Provider/AuthenticationProvider.php @@ -0,0 +1,55 @@ + + */ +class AuthenticationProvider { + + private $userProvider; + + public function __construct(UserProviderInterface $userProvider) { + $this->userProvider = $userProvider; + } + + public function authenticate() { + $identifier = config('identifier'); + $identifierFormField = config('login-form-identifier-field'); + $passwordFormField = config('login-form-password-field'); + if (!in_array($identifier, ['email', 'username'], true)) { + throw new SecurityException(_t('security.error.bad-identifier', config('lang'))); + } + + if (!empty($_SESSION[$identifier])) { + $user = $this->userProvider->loadUser($_SESSION[$identifier]); + return $user; + } + + if (!empty($_POST[$identifierFormField])) { + $user = $this->userProvider->loadUser($_POST[$identifierFormField]); + if (password_verify($_POST[$passwordFormField], $user->getPassword())) { + if ($identifier === 'email') { + $_SESSION[$identifier] = $user->getEmail(); + } elseif ($identifier === 'username') { + $_SESSION[$identifier] = $user->getUsername(); + } + header('Location: ' . config('redirect-path')); + exit; + } + http_response_code(401); + throw new SecurityException(_t('security.error.bad-input', config('lang'))); + } + http_response_code(401); + throw new UserNotAuthenticatedException(_t('security.error.not-logged-in', config('lang'))); + } + +} diff --git a/src/User.php b/src/User.php deleted file mode 100644 index 95e03b4..0000000 --- a/src/User.php +++ /dev/null @@ -1,76 +0,0 @@ - - */ -class User implements UserInterface { - - private $id; - private $username; - private $password; - private $email; - private $name; - private $surname; - private $roles; - - public function getId(): ?int { - return $this->id; - } - - public function getUsername(): string { - return $this->username; - } - - public function getPassword(): string { - return $this->password; - } - - public function getEmail(): string { - return $this->email; - } - - public function getName(): ?string { - return $this->name; - } - - public function getSurname(): ?string { - return $this->surname; - } - - public function getRoles(): array { - return $this->roles; - } - - public function setId(int $id) { - $this->id = $id; - } - - public function setUsername(string $username) { - $this->username = $username; - } - - public function setPassword(string $password) { - $this->password = $password; - } - - public function setEmail(string $email) { - $this->email = $email; - } - - public function setName(string $name) { - $this->name = $name; - } - - public function setSurname(string $surname) { - $this->surname = $surname; - } - - public function setRoles(array $roles) { - $this->roles = $roles; - } - -} diff --git a/tests/InMemoryUserProviderTest.php b/tests/InMemoryUserProviderTest.php deleted file mode 100644 index 77f86e0..0000000 --- a/tests/InMemoryUserProviderTest.php +++ /dev/null @@ -1,21 +0,0 @@ -expectException(Exception::class); - - $users = unserialize(file_get_contents(__DIR__ . '/data/users')); - $userProvider = new \Kavalanche\Security\InMemoryUserProvider($users); - $userProvider->loadUserByUsername('Kasia'); - } - - public function testLoadUserByUsername_returnsUser() { - $users = unserialize(file_get_contents(__DIR__ . '/data/users')); - $userProvider = new \Kavalanche\Security\InMemoryUserProvider($users); - $user = $userProvider->loadUserByUsername('Wojtek'); - - $this->assertInstanceOf(\Kavalanche\Security\UserInterface::class, $user); - } - -} diff --git a/tests/data/users b/tests/data/users deleted file mode 100644 index 3dad48d..0000000 Binary files a/tests/data/users and /dev/null differ diff --git a/translations/en.yaml b/translations/en.yaml new file mode 100644 index 0000000..605cee1 --- /dev/null +++ b/translations/en.yaml @@ -0,0 +1,3 @@ +security.error.bad-identifier: 'User identifier must be one of: email, username' +security.error.bad-input: 'Wrong data provided' +security.error.not-logged-in: 'You have to be logged in' diff --git a/translations/pl.yaml b/translations/pl.yaml new file mode 100644 index 0000000..a8951f8 --- /dev/null +++ b/translations/pl.yaml @@ -0,0 +1,3 @@ +security.error.bad-identifier: 'Możliwe wartości identyfikatora: email, username' +security.error.bad-input: 'Nieprawidłowe dane' +security.error.not-logged-in: 'Musisz być zalogowany' diff --git a/translations/translations.php b/translations/translations.php new file mode 100644 index 0000000..e9f1a36 --- /dev/null +++ b/translations/translations.php @@ -0,0 +1,11 @@ +