Framework supports form based and OAuth2 based (eg: Facebook) authentication, using databases or XML to store users and authorize access to resources. To make any of above possible, you need to open bootstrap index.php file and add this event listener:
$object->addEventListener(Lucinda\STDOUT\EventType::REQUEST, Lucinda\Project\EventListeners\Security::class);
then create a <security> tag in XML root where authentication, authorization and state persistence are configured. To learn more how to configure this tag, check official documentation!
Below example sets up a csrf token protected form based authentication where credentials are checked in database (by Users class in src/DAO folder) and access rights in XML while logged in state persists in session as uid param:
<security>
<csrf secret="SECRET_KEY"/>
<authentication>
<form dao="Lucinda\Project\DAO\UserAuthentication" throttler="Lucinda\Project\DAO\NoSqlLoginThrottler"/>
</authentication>
<persistence>
<session/>
</persistence>
<authorization>
<by_route/>
</authorization>
</security>
If you desire to use XML-based authentication instead, simply remove dao attribute above and make sure tag <users> is properly set in stdout.xml!
To prevent cross site scripting, make sure value of secret attribute is unique for your site! You can use Lucinda\WebSecurity\Token\SaltGenerator to generate it:
$saltGenerator = new \Lucinda\WebSecurity\Token\SaltGenerator(128);
$secret = $saltGenerator->getSalt();
To prevent password-guessing through brute-force attacks, it is required to provide a throttler attribute pointing to a class in src/DAO folder that ultimately extends Lucinda\WebSecurity\Authentication\Form\LoginThrottler.
If you're using a Lucinda\Framework\AbstractLoginThrottler implementation provided by framework, it will ban wrongdoer from attempting to log in for 2ATTEMPTS_NUMBER-1 seconds at every consecutive failure. Framework comes already with following implementations:
CREATE TABLE user_logins (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
ip VARCHAR(45) NOT NULL,
username VARCHAR(255) NOT NULL,
attempts BIGINT UNSIGNED NOT NULL default 0,
penalty_expiration DATETIME DEFAULT NULL,
date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(id),
UNIQUE(ip, username)
) Engine=InnoDB
Form based authentication requires at least following routes being set:
Example setting:
<routes roles="GUEST">
<route id="index" controller="Lucinda\Project\Controllers\Index" view="index" roles="GUEST,MEMBER">
<route id="login" controller="Lucinda\Project\Controllers\Login" view="login" roles="GUEST">
<route id="logout" roles="MEMBER">
...
</routes>
Below example sets up a oauth2 based authentication, using database to store access tokens and check access rights (by Users class in src/DAO folder) then persisting logged in state in session as uid:
<security>
<authentication>
<oauth2 dao="Lucinda\Project\DAO\UserAuthentication"/>
</authentication>
<persistence>
<session/>
</persistence>
<authorization>
<by_route/>
</authorization>
</security>
OAuth2 based authentication requires <oauth2> tag already filled (see tutorial for more info)!
OAuth2 based authentication also requires at least following routes being set
Example setting:
<routes roles="GUEST,MEMBER">
<route id="index" controller="Lucinda\Project\Controllers\Index" view="index" roles="GUEST,MEMBER">
<route id="login/facebook" roles="GUEST">
<route id="login/google" roles="GUEST">
<route id="logout" roles="MEMBER">
...
</routes>
Above examples use XML-based authorization via <by_route> tag, which fits the needs of pretty much all normal sites. If you are developing a CMS and desire to use database-based authorization instead, use <by_dao> instead:
<authorization>
<by_dao page_dao="Lucinda\Project\DAO\PagesAuthorization" user_dao="Lucinda\Project\DAO\UsersAuthorization"/>
</authorization>
If you are using this authorization method, roles attribute @ <route> is not needed!
In order to preserve logged in state across requests, you must work on <persistence> tag. Example:
<persistence>
<session/>
<remember_me secret="P*tD:MKpTeBU?K]"/>
</persistence>
This persists logged in state in both session and remember me cookie, protected against cross site request forgery through an expiring secret token bound to ip. To prevent cross site scripting, make sure value of secret is unique for your site!
After you have completed Setting Authentication and Authorization section described above, you have a <security> tag that sets up authentication, pointing to classes developers must implement in order for framework to handle authentication.
Assuming XML is same as in Setting Authentication and Authorization:
<security>
...
<authentication>
<form dao="Lucinda\Project\DAO\UsersFormAuthentication" throttler="Lucinda\Project\DAO\NoSqlLoginThrottler"/>
</authentication>
...
</security>
Above sets a form authentication, where credentials are checked in database by class referenced by dao attribute in src/DAO folder implementing Lucinda\WebSecurity\Authentication\DAO\UserAuthenticationDAO.
Demo implementation example @ Framework Configurer API:
Lucinda\Project\DAO\UsersFormAuthentication
Above example assumes following recommended minimal MySQL table structure:
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
PRIMARY KEY(id),
UNIQUE(username)
) Engine=INNODB;
In order to prevent cross site request forgery, login forms must also include a CSRF token:
<form action="" method="POST">
Username: <input type="text" name="username" required/>
Password: <input type="password" name="password" required/>
<input type="checkbox" name="remember_me" value="1" checked/> Remember me
<input type="hidden" name="csrf" value="${data.csrf}"/>
<input type="submit" value="LOGIN"/>
<a href="/">HOMEPAGE</a>
</form>
Where value of csrf must sent to view by controller as below:
class LoginController extends Lucinda\STDOUT\Controller
{
public function run(): void
{
$this->response->view()["csrf"] = $this->attributes->getCsrfToken();
}
}
Assuming XML is same as in Setting Authentication and Authorization:
<security>
...
<authentication>
<form throttler="Lucinda\Project\DAO\NoSqlLoginThrottler"/>
</authentication>
...
</security>
You need to set existing users in <users> tag @ stdout.xml. Example:
<users>
<user id="1" username="john" password="PASSWORD" roles="MEMBER"/>
<user id="2" username="mark" password="PASSWORD" roles="MEMBER,ADMINISTRATOR"/>
</users>
Where each user PASSWORD in XML must be a password_hash result:
$passwordToSave = password_hash($originalPassword, PASSWORD_DEFAULT);
Assuming XML is same as in Setting Authentication and Authorization:
<security>
...
<authentication>
<oauth2 dao="Lucinda\Project\DAO\UsersOAuth2Authentication"/>
</authentication>
...
</security>
Above sets a OAuth2 authentication where access token is saved in database by class referenced by dao attribute in src/DAO folder implementing Lucinda\WebSecurity\Authentication\OAuth2\VendorAuthenticationDAO.
Demo implementation example @ Framework Configurer API:
Lucinda\Project\DAO\UsersOAuth2Authentication
Above example assumes following recommended minimal MySQL table structure:
CREATE TABLE oauth2_providers (
id TINYINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
PRIMARY KEY(id),
UNIQUE(name)
) Engine=INNODB;
INSERT INTO oauth2_providers (name) VALUES ('Facebook'), ('Google'), ('GitHub'), ('Instagram'), ('LinkedIn'), ('VK'), ('Yahoo'), ('Yandex');
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
driver_id TINYINT UNSIGNED NOT NULL,
remote_user_id VARCHAR(32) NOT NULL,
access_token TEXT NOT NULL,
PRIMARY KEY(id),
FOREIGN KEY(driver_id) REFERENCES oauth2_providers(id),
UNIQUE(email),
UNIQUE(driver_id, remote_user_id)
) Engine=INNODB CHARACTER SET utf8 COLLATE utf8_bin;
Some sites require you to support both form and oauth2 methods of authentication. In that case XML should be like:
<security>
...
<authentication>
<form dao="Lucinda\Project\DAO\UsersFormAuthentication" throttler="Lucinda\Project\DAO\NoSqlLoginThrottler"/>
<oauth2 dao="Lucinda\Project\DAO\UsersOAuth2Authentication"/>
</authentication>
...
</security>
where DAO classes, located in folder src/DAO.
Demo implementation examples @ Framework Configurer API:
Lucinda\Project\DAO\UsersFormAuthentication +
Lucinda\Project\DAO\UsersOAuth2Authentication
assuming following MySQL table structure:
CREATE TABLE users (
id INT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL,
PRIMARY KEY(id),
UNIQUE(email)
) Engine=INNODB CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE users__form (
user_id INT UNSIGNED NOT NULL,
username VARCHAR(255) NOT NULL,
password VARCHAR(255) NOT NULL,
PRIMARY KEY(user_id),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
UNIQUE(username)
) Engine=INNODB CHARACTER SET utf8 COLLATE utf8_bin;
CREATE TABLE users__oauth2 (
user_id INT UNSIGNED NOT NULL,
driver_id TINYINT UNSIGNED NOT NULL,
remote_user_id VARCHAR(32) NOT NULL,
access_token TEXT NOT NULL,
PRIMARY KEY(user_id),
FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
FOREIGN KEY(driver_id) REFERENCES oauth2_providers(id) ON DELETE CASCADE,
UNIQUE(driver_id, remote_user_id)
) Engine=INNODB;
After authentication is performed, a Lucinda\WebSecurity\SecurityPacket is thrown for STDERR MVC API to handle via Lucinda\Project\Controllers\SecurityPacket which will issue redirections on success/failure. If successful, you can from now on get id of logged in user in controller or subsequent event listener using:
$userID = $this->attributes->getUserId();
Once user logs in, its id is saved into persistence drivers (eg: session, cookie) defined in <persistence> tag.
After you have completed Setting Authentication and Authorization section described above, you already have a <security> tag that sets up authorization depending on solution chosen. Once a request is received, if user is authorized access to requested site resource, execution continues. Otherwise, a Lucinda\WebSecurity\SecurityPacket is thrown for STDERR MVC API to handle results (eg: issue redirections).
Assuming XML is same as in Setting Authentication and Authorization:
<security>
...
<authorization>
<by_route/>
</authentication>
...
</security>
Above sets an authorization where access rights are checked in XML based on contents of <routes> tag @ stdout.xml. This makes homepage accessible by all users, login only for unauthenticated guests, logout/members only for authenticated users.
Matching route roles to roles held by user depends on authentication solution chosen:
Assuming XML is same as in Setting Authentication and Authorization:
<security>
...
<authorization>
<by_dao page_dao="Lucinda\Project\DAO\PagesAuthorization" user_dao="Lucinda\Project\DAO\UsersAuthorization">
</authentication>
...
</security>
Above sets a DAO authorization, where access rights are matched in database by classes referenced by page_dao and user_dao attributes found in src/DAO folder and extending Lucinda\WebSecurity\Authorization\DAO\PageAuthorizationDAO and Lucinda\WebSecurity\Authorization\DAO\UserAuthorizationDAO respectively.
Demo implementation examples @ Framework Configurer API:
Lucinda\Project\DAO\PagesAuthorization +
Lucinda\Project\DAO\UsersAuthorization
assuming following MySQL table structure:
CREATE TABLE users_resources
(
id int unsigned not null auto_increment,
user_id int unsigned not null,
resource_id smallint unsigned not null,
PRIMARY KEY(id),
foreign key(user_id) references users(id) on delete cascade,
foreign key(resource_id) references resources(id) on delete cascade
) Engine=INNODB