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. They are available in following combinations:

To make any of above possible, open stdout.xml and make sure this tag exists within <listeners>:

<listener class="SecurityListener"/>

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!

Configuring Form-based Authentication

Below example sets up a csrf token protected form based authentication where credentials are checked in database (by Users classe 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="SqlLoginThrottler"/> </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 <routes> is properly set in stdout.xml!

Form based authentication requires at least following routes being set:

Example setting:

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

Apart of requiring developers to set routes, framework will automatically manage authentication. Of above, the only route requiring site-specific development is index whereas login just requires a form that POSTs to itself (see Authentication).

Securing Form-based Authentication

To prevent password-guessing through brute-force attacks, it is required to provide a throttler attribute pointing to a class in application/models/throttlers folder that ultimately extends Lucinda\Framework\FormLoginThrottler! Example:

<form dao="Users" throttler="SqlLoginThrottler"/>

If you're using a BasicLoginThrottler implementation provided by framework (SqlLoginThrottler or NoSqlLoginThrottler), this will ban wrongdoer from attempting to log in for 2ATTEMPTS_NUMBER-1 seconds at every consecutive failure.

One problem with all throttlers is that they are based on username-IP combination and IPs sent as HTTP headers (X-Forwarded-For) can be easily spoofed. To prevent that from happening, open SecurityListener and make this change:

$securityFilter = new Lucinda\Framework\SecurityBinder($this->application, $this->request, ENVIRONMENT, true);

This will insure IPs will only be detected by value of REMOTE_ADDR, which requires consistent effort to be spoofed. However, the major problem with this is that ips of legitimate users that came via proxies won't be detected properly.

Configuring OAuth2-based Authentication

Below example sets up on live development environment a Facebook-based oauth2 based authentication, using database to store access tokens and check access rights (by Users & Pages classes in application/models/dao folder) then persisting logged in state in session as uid:

<security dao_path="application/models/dao"> <authentication> <oauth2 dao="Users"> <live> <driver name="Facebook" client_id="APP_ID" client_secret="APP_SECRET" callback="login/facebook"/> </live> </oauth2> </authentication> <persistence> <session/> </persistence> <authorization> <by_route/> </authorization> </security>

For more information where to get above APP_ID (client id), APP_SECRET (client secret) and CALLBACK_URI (redirect uri) from, check framework documentation! To support another oauth2 provider as well, add another <driver> tag to <oauth2>:

<oauth2 dao="Users"> <live> <driver name="Facebook" client_id="APP_ID_1" client_secret="APP_SECRET_1" callback="login/facebook"/> <driver name="Google" client_id="APP_ID_2" client_secret="APP_ID_2" callback="login/google"/> </live> </oauth2>

To use an oauth2 provider for more than just authentication, make sure scope attribute is setup for matching <driver> tag above according to framework documentation. Providers require each scope to be approved by your site end user!

OAuth2 based authentication requires at least following routes being set:

Example setting:

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

Apart of requiring developers to set routes, framework will automatically manage authentication. Of above, the only route requiring site-specific development is index.

Configuring Authorization

Above examples use XML-based authorization, which fits the needs of pretty much all normal sites. If you are developing a CMS and desire to use database-based authorization instead, replace <by_route> tag above with:

<by_dao page_dao="PagesAuthorization" user_dao="UsersAuthorization">

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 <security> 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="SqlLoginThrottler"/> </authentication> ... </security>

Above sets a form authentication, where credentials are checked in database by class Users found in application/models/dao folder implementing Lucinda\WebSecurity\UserAuthenticationDAO. Example:

class UsersFormAuthentication implements Lucinda\WebSecurity\UserAuthenticationDAO { public function login($username, $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() { } }

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\MVC\STDOUT\Controller { public function run() { $this->response->attributes("csrf", $this->request->attributes("csrf")->generate(0)); } }

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 <users> tag @ stdout.xml. Example:

<users> <user id="1" username="john" password="PASSWORD" roles="MEMBERS"/> <user id="2" username="mark" password="PASSWORD" roles="MEMBERS,ADMINISTRATORS"/> </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"> <live> <driver name="Facebook" client_id="APP_ID" client_secret="APP_SECRET" callback="login/facebook"/> </live> </oauth2> </authentication> ... </security>

Above sets a OAuth2 authentication using Facebook, where latter access token is saved in database by class Users found in application/models/dao folder implementing Lucinda\WebSecurity\OAuth2AuthenticationDAO. Recommended:

class UsersOAuth2Authentication implements Lucinda\WebSecurity\OAuth2AuthenticationDAO { public function login(Lucinda\WebSecurity\OAuth2UserInformation $userInformation, $accessToken) { // get driver ID $driver = str_replace(array("Lucinda\\Framework\\", "UserInformation"), "", get_class($userInformation)); $driverID = SQL("SELECT id FROM oauth2_providers WHERE name=:driver", array(":driver"=>$driver))->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", array(":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", array(":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", array(":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)", array(":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) { SQL("UPDATE users__oauth2 SET access_token = '' WHERE user_id = :user_id", array(":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"> <driver name="Facebook" client_id="APP_ID" client_secret="APP_SECRET" callback="login/facebook"/> </oauth2> </authentication> ... </security>

where DAO classes should be:

class UsersFormAuthentication implements Lucinda\WebSecurity\UserAuthenticationDAO { public function login($username, $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() { } } class UsersOAuth2Authentication implements Lucinda\WebSecurity\OAuth2AuthenticationDAO { public function login(Lucinda\WebSecurity\OAuth2UserInformation $userInformation, $accessToken) { // get driver ID $driver = str_replace(array("Lucinda\\Framework\\", "UserInformation"), "", get_class($userInformation)); $driverID = SQL("SELECT id FROM oauth2_providers WHERE name=:driver", array(":driver"=>$driver))->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 Lucinda\Framework\SecurityPacket 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 as user_id attribute value.

$userID = $this->application->attributes("user_id");

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 <security> tag that sets up authorization depending on solution chosen.

If user is authorized access to requested site resource, execution continues. Otherwise, a Lucinda\Framework\SecurityPacket 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 <routes> tag @ stdout.xml. Example of tag body:

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

This makes homepage accessible by all users, login only for unauthenticated guests, logout/members only for authenticated users. Matching these rules 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 checked in database by classes Users and Pages found in application/models/dao folder. Class Users must extend Lucinda\WebSecurity\UserAuthorizationDAO. Example:

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

While class Pages must extend Lucinda\WebSecurity\PageAuthorizationDAO. Example:

class PagesAuthorization extends Lucinda\WebSecurity\PageAuthorizationDAO { public function isPublic() { return SQL("SELECT is_public FROM resources WHERE id=:id", array(":id"=>$this->pageID))->toValue(); } public function detectID($path) { return SQL("SELECT id FROM resources WHERE url=:url", array(":url"=>$path))->toValue(); } }

Integrating OAuth2 Providers

Framework supports integration of providers, described in great detail here, through a common driver built on OAuth2 standards without resorting to official PHP drivers (often bloated and overprogrammed).

Registration and Authentication

If you only need OAuth2 integration for authentication (eg: login by Facebook), then these steps are all you need:

  1. Registering your site on OAuth2 provider in order to get a app_id, app_secret to be used when logging in
    For more info, go to Registration section.
  2. Setting up XML for OAuth2 authentication on that provider using app_id and app_secret obtained above
    For more info, go to Configuration section.
  3. Developing DAO classes XML points to in order to persist users and access tokens into database
    For more info, go to Authentication section.

Getting Remote Resources

In order to be able to retrieve remote resources owned by user on oauth2 provider site (eg: Facebook friends), these steps are to be followed:

  1. Go to Scopes section, follow link to provider and get scopes associated to that resource.
    For example, to get Facebook photos, user_photos scope will be required.
  2. Append scope found above to scopes attribute of matching <driver> XML tag separated using comma. Example:
    <driver name="facebook" scopes="user_photos" client_id="..." client_secret="..." callback="login/facebook"/>
  3. When user logs in, let him/her approve site access to that remote resource they own (eg: their Facebook photos)
  4. Make class referenced by dao attribute of <oauth2> tag also implement Lucinda\Framework\OAuth2ResourcesDAO. Example (assuming only OAuth2 authentication is used):
    class UsersOAuth2Authentication implements Lucinda\WebSecurity\OAuth2AuthenticationDAO, Lucinda\Framework\OAuth2ResourcesDAO { ... public function getDriverName($userID) { return SQL("SELECT t1.name FROM oauth2_providers AS t1 INNER JOIN users AS t2 ON t1.id = t2.driver_id WHERE t2.id = :user_id", array(":user_id"=>$userID))->toValue(); } public function getAccessToken($userID) { return SQL("SELECT access_token FROM users WHERE id = :user_id", array(":user_id"=>$userID))->toValue(); } }
  5. Once authentication completes, to get remote resource, use oauth2 attribute provided and resource url. Example:
    $this->application->attributes("oauth2")->getResource("https://graph.facebook.com/me/photos");

Handling Errors

Framework wraps any error received from provider (during authentication or resources retrieval) into an OAuth2\ServerException, which is automatically handled via this tag <exception> @ stderr.xml:

<exception class="OAuth2\ServerException" error_type="SERVER" controller="ErrorsController">

Developers may need to fine tune this behavior by using a specialized controller and/or setting a view. For more information in how to do it, check tag documentation and articles at Basic User Guide above!


Share