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:
@@ -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
|
||||
+1
-1
@@ -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)) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
identifier: "email"
|
||||
login_form_identifier_field: "email"
|
||||
login_form_password_field: "password"
|
||||
redirect_path: "/"
|
||||
login-form-identifier-field: "email"
|
||||
login-form-password-field: "password"
|
||||
redirect-path: "/"
|
||||
lang: en
|
||||
|
||||
BIN
Binary file not shown.
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>';
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
|
||||
namespace Kavalanche\Security;
|
||||
namespace Kavalanche\Security\Exception;
|
||||
|
||||
/**
|
||||
* @author Wojciech Burda <wojciech.burda@rp.pl>
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
|
||||
namespace Kavalanche\Security;
|
||||
namespace Kavalanche\Security\Exception;
|
||||
|
||||
/**
|
||||
* @author Wojciech Burda <wojciech.burda@rp.pl>
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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')));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
Binary file not shown.
@@ -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'
|
||||
@@ -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'
|
||||
@@ -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];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user