Tutorials: Authenticating and Authorizing

Setting Authentication and Authorization

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(::REQUEST, "SecurityListener");

then create a tag in XML root where authentication, authorization and state persistence are configured. To learn more how to configure this tag, check official documentation!

Configuring Form-based Authentication

Below example sets up a csrf token protected form based authentication where credentials are checked in database (by Users class in application/models/dao folder) and access rights in XML while logged in state persists in session as uid param:

<security dao_path="application/models/dao"> <csrf secret="SECRET_KEY"/> <authentication> <form dao="Users" throttler="NoSqlLoginThrottler"/> </authentication> <persistence> <session/> </persistence> <authorization> <by_route/> </authorization> </security>

To prevent cross site scripting, make sure value of secret attribute is unique for your site! If you desire to use XML-based authentication instead, simply remove dao attribute above and make sure tag is properly set in stdout.xml!

To prevent password-guessing through brute-force attacks, it is required to provide a throttler attribute pointing to a class in application/models/dao folder that ultimately extends . If you're using a implementation provided by framework (SqlLoginThrottler or NoSqlLoginThrottler), it will ban wrongdoer from attempting to log in for 2ATTEMPTS_NUMBER-1 seconds at every consecutive failure.

Form based authentication requires at least following routes being set:

Example setting:

<routes roles="GUEST"> <route url="index" controller="IndexController" view="index" roles="GUEST,MEMBER"> <route url="login" controller="LoginController" view="login" roles="GUEST"> <route url="logout" roles="MEMBER"> ... </routes>

Configuring OAuth2-based Authentication

Below example sets up a oauth2 based authentication, using database to store access tokens and check access rights (by Users class in application/models/dao folder) then persisting logged in state in session as uid:

<security dao_path="application/models/dao"> <authentication> <oauth2 dao="Users"/> </authentication> <persistence> <session/> </persistence> <authorization> <by_route/> </authorization> </security>

OAuth2 based authentication requires tag already filled (see tutorial for more info) and at least following routes being set:

Example setting:

<routes roles="GUEST,MEMBER"> <route url="index" controller="IndexController" view="index" roles="GUEST,MEMBER"> <route url="login/facebook" roles="GUEST"> <route url="login/google" roles="GUEST"> <route url="logout" roles="MEMBER"> ... </routes>

Configuring Authorization

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="PagesAuthorization" user_dao="UsersAuthorization"/> </authorization>

If you are using this authorization method, roles attribute @ is not needed!

Persisting Logged-in State

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!

Authenticating Users

After you have completed Setting Authentication and Authorization section described above, you have a tag that sets up authentication, pointing to classes developers must implement in order for framework to handle authentication.

Form Authentication Using Database

Assuming XML is same as in Setting Authentication and Authorization:

<security dao_path="application/models/dao"> ... <authentication> <form dao="UsersFormAuthentication" throttler="NoSqlLoginThrottler"/> </authentication> ... </security>

Above sets a form authentication, where credentials are checked in database by class referenced by dao attribute in application/models/dao folder implementing . Example:

use as UserAuthenticationDAO; class UsersFormAuthentication implements UserAuthenticationDAO { public function login(string $username, string $password) { $result = SQL("SELECT id, password FROM users WHERE username=:user",array(":user"=>$username))->toRow(); if (empty($result) || !password_verify($password, $result["password"])) { return null; // login failed } return $result["id"]; } public function logout($userID): void { } }

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 { public function run(): void { $this->response->view()["csrf"] = $this->attributes->getCsrfToken(); } }

Form Authentication Using XML

Assuming XML is same as in Setting Authentication and Authorization:

<security dao_path="application/models/dao"> ... <authentication> <form throttler="SqlLoginThrottler"/> </authentication> ... </security>

You need to set existing users in tag @ stdout.xml. Example:

<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);

OAuth2 Authentication Using Database

Assuming XML is same as in Setting Authentication and Authorization:

<security dao_path="application/models/dao"> ... <authentication> <oauth2 dao="UsersOAuth2Authentication"/> </authentication> ... </security>

Above sets a OAuth2 authentication where access token is saved in database by class referenced by dao attribute in application/models/dao folder implementing . Example:

use as UserInformation; use as VendorAuthenticationDAO; class UsersOAuth2Authentication implements VendorAuthenticationDAO { public function login(UserInformation $userInformation, string $vendorName, string $accessToken) { // get driver ID $driverID = SQL("SELECT id FROM oauth2_providers WHERE name=:driver", [":driver"=>$vendorName])->toValue(); // detects user based on driver and remote id $userID = SQL("SELECT id FROM users WHERE driver_id=:driver AND remote_user_id=:remote_user", [":driver"=>$driverID, ":remote_user"=>$userInformation->getId()])->toValue(); if ($userID) { SQL("UPDATE users SET access_token=:access_token WHERE id = :user_id", array(":user_id"=>$userID, ":access_token"=>$accessToken)); return $userID; } // detects user based on email $userID = SQL("SELECT id FROM users WHERE email=:email", [":email"=>$userInformation->getEmail()])->toValue(); if ($userID) { SQL("UPDATE users SET remote_user_id = :remote_user, driver_id = :driver, access_token = :access_token WHERE id = :user_id", [":user_id"=>$userID, ":remote_user"=>$userInformation->getId(), ":driver"=>$driverID, ":access_token"=>$accessToken]); return $userID; } // creates user $userID = SQL("INSERT INTO users (name, email, remote_user_id, driver_id, access_token) VALUES (:name, :email)", [":name"=>$userInformation->getName(), ":email"=>$userInformation->getEmail(), ":user_id"=>$userID, ":remote_user"=>$userInformation->getId(), ":driver"=>$driverID, ":access_token"=>$accessToken])->getInsertId(); return $userID; } public function logout($userID): void { SQL("UPDATE users__oauth2 SET access_token = '' WHERE user_id = :user_id", [":user_id"=>$userID]); } }

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;

Both Form & OAuth2 Authentication Using Database

Some sites require you to support both form and oauth2 methods of authentication. In that case XML should be like:

<security dao_path="application/models/dao"> ... <authentication> <form dao="UsersFormAuthentication" throttler="SqlLoginThrottler"/> <oauth2 dao="UsersOAuth2Authentication"/> </authentication> ... </security>

where DAO classes, located in folder application/models/dao, should be:

use as UserAuthenticationDAO; class UsersFormAuthentication implements UserAuthenticationDAO { public function login(string $username, string $password) { $result = SQL("SELECT id, password FROM users__form WHERE username=:user",array(":user"=>$username))->toRow(); if (empty($result) || !password_verify($password, $result["password"])) { return null; // login failed } return $result["id"]; } public function logout(): void { } }

and:

use as UserInformation; use as VendorAuthenticationDAO; class UsersOAuth2Authentication implements VendorAuthenticationDAO { public function login(UserInformation $userInformation, string $vendorName, string $accessToken) { // get driver ID $driverID = SQL("SELECT id FROM oauth2_providers WHERE name=:driver", array(":driver"=>$vendorName))->toValue(); // detects user based on driver and remote id $userID = SQL("SELECT user_id FROM users__oauth2 WHERE driver_id=:driver" AND remote_user_id=:remote_user, array(":driver"=>$driverID, ":remote_user"=>$userInformation->getId()))->toValue(); if ($userID) { SQL("UPDATE users__oauth2 SET access_token=:access_token WHERE user_id = :user_id", array(":user_id"=>$userID, ":access_token"=>$accessToken)); return $userID; } // detects user based on email $userID = SQL("SELECT id FROM users WHERE email=:email", array(":email"=>$userInformation->getEmail()))->toValue(); if ($userID) { SQL("REPLACE INTO users__oauth2 (user_id, remote_user_id, driver_id, access_token) VALUES (:user_id, :remote_user, :driver, :access_token)", array(":user_id"=>$userID, ":remote_user"=>$userInformation->getId(), ":driver"=>$driverID, ":access_token"=>$accessToken)); return $userID; } // creates user $userID = SQL("INSERT INTO users (name, email) VALUES (:name, :email)", array(":name"=>$userInformation->getName(), ":email"=>$userInformation->getEmail()))->getInsertId(); SQL("INSERT INTO users__oauth2 (user_id, remote_user_id, driver_id, access_token) VALUES (:user_id, :remote_user, :driver, :access_token)", array(":user_id"=>$userID, ":remote_user"=>$userInformation->getId(), ":driver"=>$driverID, ":access_token"=>$accessToken)); return $userID; } public function logout($userID) { SQL("UPDATE users__oauth2 SET access_token = '' WHERE user_id = :user_id", array(":user_id"=>$userID)); } }

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;

Getting Logged In User ID

After authentication is performed, a is thrown for STDERR MVC API to handle results (eg: 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.

Authorizing User Access to Site Resources

After you have completed Setting Authentication and Authorization section described above, you already have a 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 is thrown for STDERR MVC API to handle results (eg: issue redirections).

Authorization Using XML

Assuming XML is same as in Setting Authentication and Authorization:

<security dao_path="application/models/dao"> ... <authorization> <by_route/> </authentication> ... </security>

Above sets an authorization where access rights are checked in XML based on contents of 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:

Authorization Using Database

Assuming XML is same as in Setting Authentication and Authorization:

<security dao_path="application/models/dao"> ... <authorization> <by_dao page_dao="PagesAuthorization" user_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 application/models/dao folder and extending and respectively. Examples:

use as PageAuthorizationDAO; class PagesAuthorization extends PageAuthorizationDAO { public function isPublic(): bool { return (bool) SQL("SELECT is_public FROM resources WHERE id=:id", array(":id"=>$this->pageID))->toValue(); } protected function detectID(string $pageURL): ?int { $id = SQL("SELECT id FROM resources WHERE url=:url", array(":url"=>$pageURL))->toValue(); return ($id?(int) $id:null); } }

and:

use as UserAuthorizationDAO; class UsersAuthorization extends UserAuthorizationDAO { public function isAllowed( $page, string $httpRequestMethod): bool { return (bool) SQL("SELECT id FROM users_resources WHERE resource_id=:resource AND user_id=:user", array(":user"=>$this->userID, ":resource"=>$page->getID()))->toValue(); } }
×