Introduces breaking changes

Changes directory structure in src
Renames parameters in config
Removes built in User providers
Adds translations to errors
Removes docker and public files
This commit is contained in:
2022-02-27 01:53:35 +01:00
parent 7b23ea3aab
commit 6cfe0ded85
21 changed files with 92 additions and 392 deletions
+13 -28
View File
@@ -9,24 +9,12 @@ Simple security library for web applications.
composer require kavalanche/security 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 ```php
// $users must be an array of objects implementing Kavalanche\Security\UserInterface $authenticationProvider = new Kavalanche\Security\Provider\AuthenticationProvider($userProvider);
$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);
``` ```
4. Check if user is authenticated. 4. Check if user is authenticated.
@@ -34,33 +22,33 @@ Simple security library for web applications.
```php ```php
try { try {
$user = $authenticationProvider->authenticate(); $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 want to allow unauthenticated users, then assign false or null to $user
// if you require user to be authenticated do as follows // 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 // put message in flash session and redirect to user form
// or do whatever your use case demands // 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`. 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: 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-identifier-field` for identifier field
- `login_form_password_field` for password field - `login-form-password-field` for password field
5. Don't forget to put `session_start()` at the beginning of your file. 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 ```php
class UserProvider implements UserProviderInterface { class UserProvider implements Kavalanche\Security\Interface\UserProviderInterface {
public function loadUser($identifier) { public function loadUser($identifier) {
@@ -72,7 +60,7 @@ class UserProvider implements UserProviderInterface {
return $user; 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. - 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. - 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. - 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 # To do
- Implement some kind of request abstraction to encapsulate Requests (symfony/http-foundation?) - Implement some kind of request abstraction to encapsulate Requests (symfony/http-foundation?)
- Add helper to check permissions - Add helper to check permissions
- Translate exception messages
+1 -1
View File
@@ -2,7 +2,7 @@
namespace Kavalanche\Security; namespace Kavalanche\Security;
function getValueFromConfig(string $key) { function config(string $key) {
$config = yaml_parse_file(__DIR__ . '/parameters.yaml'); $config = yaml_parse_file(__DIR__ . '/parameters.yaml');
$userConfigPath = __DIR__ . '/../../../../config/security.yaml'; $userConfigPath = __DIR__ . '/../../../../config/security.yaml';
if (file_exists($userConfigPath)) { if (file_exists($userConfigPath)) {
+4 -3
View File
@@ -1,4 +1,5 @@
identifier: "email" identifier: "email"
login_form_identifier_field: "email" login-form-identifier-field: "email"
login_form_password_field: "password" login-form-password-field: "password"
redirect_path: "/" redirect-path: "/"
lang: en
BIN
View File
Binary file not shown.
-44
View File
@@ -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 <app-secret-id>;
# 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;
}
-9
View File
@@ -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
-92
View File
@@ -1,92 +0,0 @@
<?php
require __DIR__ . '/../vendor/autoload.php';
use Kavalanche\Security\AuthenticationProvider;
use Kavalanche\Security\InMemoryUserProvider;
use Kavalanche\Security\SecurityException;
use Kavalanche\Security\UserNotAuthenticatedException;
session_start();
if (isset($_GET['logout'])) {
session_destroy();
header('Location: /');
exit;
}
function getUserArticles($user) {
$data = [
[
'user_id' => 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) {
?>
<div class="alert"><p><?php echo $ex->getMessage(); ?></p></div>
<?php
}
?>
<form method="post">
<input type="text" name="email" id="login_username"><label for="login_username">E-mail</label><br>
<input type="password" name="password" id="login_password"><label for="login_password">Password</label><br>
<input type="submit" value="Log in">
</form>
<?php
exit;
}
}
if (!empty($user)) {
echo 'Hello ' . ($user->getName() ?? "anonymous") . ' | <a href="?logout">Log out</a>';
} else {
echo '<a href="?login">Log in</a>';
}
if (!empty($user)) {
$articles = getUserArticles($user);
} else {
$articles = [];
}
foreach ($articles as $article) {
?>
<article>
<h2><?php echo $article->title; ?></h2>
<div><?php echo $article->body; ?></div>
</article>
<?php
}
if (empty($articles)) {
echo '<p>Log in to see your posts.</p>';
}
-46
View File
@@ -1,46 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security;
/**
* @author Wojciech Burda <wojciech.burda@rp.pl>
*/
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');
}
}
-40
View File
@@ -1,40 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security;
use PDO;
/**
* @author Wojciech Burda <wojciech.burda@rp.pl>
*/
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');
}
}
@@ -1,6 +1,6 @@
<?php <?php
namespace Kavalanche\Security; namespace Kavalanche\Security\Exception;
/** /**
* @author Wojciech Burda <wojciech.burda@rp.pl> * @author Wojciech Burda <wojciech.burda@rp.pl>
@@ -1,6 +1,6 @@
<?php <?php
namespace Kavalanche\Security; namespace Kavalanche\Security\Exception;
/** /**
* @author Wojciech Burda <wojciech.burda@rp.pl> * @author Wojciech Burda <wojciech.burda@rp.pl>
-30
View File
@@ -1,30 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security;
/**
* @author Wojciech Burda <wojciech.burda@rp.pl>
*/
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');
}
}
+55
View File
@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security\Provider;
use Kavalanche\Security\Exception\SecurityException;
use Kavalanche\Security\Exception\UserNotAuthenticatedException;
use Kavalanche\Security\UserProviderInterface;
use function Kavalanche\Security\_t;
use function Kavalanche\Security\config;
/**
* @author Wojciech Burda <wojciech.burda@rp.pl>
*/
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')));
}
}
-76
View File
@@ -1,76 +0,0 @@
<?php
declare(strict_types=1);
namespace Kavalanche\Security;
/**
* @author Wojciech Burda <wojciech.burda@rp.pl>
*/
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;
}
}
-21
View File
@@ -1,21 +0,0 @@
<?php
class InMemoryUserProviderTest extends PHPUnit\Framework\TestCase {
public function testLoadUserByUsername_throwsOnError() {
$this->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);
}
}
BIN
View File
Binary file not shown.
+3
View File
@@ -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'
+3
View File
@@ -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'
+11
View File
@@ -0,0 +1,11 @@
<?php
namespace Kavalanche\Security;
function _t(string $key, string $lang = 'en') {
$translation = yaml_parse_file(__DIR__ . '/' . $lang . '.yaml');
if (!empty($translation[$key])) {
return $translation[$key];
}
}