Web Security API
About
This API is a skeleton (requires binding by developers) that implements common concerns of web security (authentication, authorization, state persistence, csrf prevention) based on OWASP guidelines,
offering multiple binding points where developers MUST plugin components using its prototypes in order to complete the process (eg: DB authentication)., (*1)
, (*2)
It does so using this series of steps:, (*3)
-
configuration: setting up an XML file where web security is configured
-
binding points: binding user-defined components defined in XML/code to API prototypes
-
execution: creating a Wrapper instance to authenticate & authorize then use it to get logged in user id, access token (for stateless apps) or csrf token (for form logins)
API is fully PSR-4 compliant, only requiring PHP 8.1+ interpreter and SimpleXML + OpenSSL extensions. To quickly see how it works, check:, (*4)
-
installation: describes how to install API on your computer, in light of steps above
-
unit tests: API has 100% Unit Test coverage, using UnitTest API instead of PHPUnit for greater flexibility
-
example: shows a deep example of API functionality based on unit test for Wrapper
All classes inside belong to Lucinda\WebSecurity namespace!, (*5)
Configuration
To configure this API you must have a XML with following tags inside:, (*6)
-
security: (mandatory) configures the api
-
users: (optional) required only if authentication is by XML (access control list)
-
routes: (optional) required only if authorization is by XML (access control list)
Security
Maximal syntax of this tag is:, (*7)
<security>
<csrf secret="..." expiration="..."/>
<persistence>
<session parameter_name="..." expiration="..." is_http_only="..." is_https_only="..." ignore_ip="..." handler="..."/>
<remember_me secret="..." parameter_name="..." expiration="..." is_http_only="..." is_https_only="..."/>
<synchronizer_token secret="..." expiration="..." regeneration="..."/>
<json_web_token secret="..." expiration="..." regeneration="..."/>
</persistence>
<authentication>
<form dao="..." throttler="...">
<login page="..." target="..." parameter_username="..." parameter_password="..." parameter_rememberMe="..." />
<logout page="..." target="..."/>
</form>
<oauth2 dao="..." target="..." login="..." logout="..."/>
</authentication>
<authorization>
<by_dao page_dao="..." user_dao="..." logged_in_callback="..." logged_out_callback="..."/>
<by_route logged_in_callback="..." logged_out_callback="..."/>
</authorization>
</security>
Where:, (*8)
-
security: (mandatory) holds global web security policies.
-
csrf: (mandatory) holds settings necessary to produce an anti-CSRF token (useful to sign authentication with)
-
secret: (mandatory) password to use in encrypting csrf token (use: Token\SaltGenerator)
-
expiration: (optional) seconds until token expires. If not set, token will expire in 10 minutes.
-
persistence (mandatory) holds one or more mechanisms useful to preserve logged in state across requests (at least one is mandatory!)
-
session: (optional) configures persistence of logged in state by HTTP session
-
parameter_name: (optional) name of $_SESSION parameter that will store logged in state. If not set, "uid" is assumed.
-
expiration: (optional) seconds until session expires. If not set, session will expire as server-default.
-
is_http_only: (optional) whether or not to set session cookie as HttpOnly (can be 0 or 1; 0 is default).
-
is_https_only: (optional) whether or not to set session cookie as HTTPS only (can be 0 or 1; 0 is default).
-
handler: (optional) name of class (incl. namespace or relative path) implementing SessionHandlerInterface to which session handling will be delegated to.
-
remember_me: (optional) configures persistence of logged in state by HTTP remember me cookie
-
secret: (mandatory) password to use in encrypting cookie (use: Token\SaltGenerator)
-
parameter_name: (optional) name of $_COOKIE parameter that will store logged in state. If not set, "uid" is assumed.
-
expiration: (optional) seconds until cookie expires. If not set, cookie will expire in one day.
-
is_http_only: (optional) whether or not to set cookie as HttpOnly (can be 0 or 1; 0 is default).
-
is_https_only: (optional) whether or not to set cookie as HTTPS only (can be 0 or 1; 0 is default).
-
synchronizer_token: (optional) configures persistence of logged in state by signing every request with a synchronizer token
-
secret: (mandatory) password to use in encrypting token (use: Token\SaltGenerator)
-
expiration: (optional) seconds until token expires. If not set, token will expire in 1 hour.
-
regeneration: (optional) seconds from the moment token was created until it needs to regenerate on continuous usage. If not set, token will be regenerated in 1 minute.
-
json_web_token: (optional) configures persistence of logged in state by signing every request with a json web token
-
secret: (mandatory) password to use in encrypting token (use: Token\SaltGenerator)
-
expiration: (optional) seconds until token expires. If not set, token will expire in 1 hour.
-
regeneration: (optional) seconds from the moment token was created until it needs to regenerate on continuous usage. If not set, token will be regenerated in 1 minute.
-
authentication: (mandatory) holds one or more mechanisms to authenticate (at least one is mandatory!)
-
form: (optional) configures authentication via form. If no dao attribute is set, authentication is done via XML and users tag is required!
-
dao: (optional) name of PSR-4 autoload-compliant class (incl. namespace) implementing Authentication\DAO\UserAuthenticationDAO that performs form authentication in database. 1
-
throttler: (optional) name of PSR-4 autoload-compliant class (incl. namespace) extending Authentication\Form\LoginThrottler that performs login throttling prevention
-
login: (optional) configures login
-
page: (optional) page that performs login operation (all requests to this page will pass through this filter), also one to redirect back if login is unsuccessful. If none, then "login" is implicitly used.
-
target: (optional) destination page after successful login. If none, then "index" is implicitly used.
-
parameter_username: (optional) name of $_POST parameter username will be submitted as. If none, then "username" is implicitly used.
-
parameter_password: (optional) name of $_POST parameter password will be submitted as. If none, then "password" is implicitly used.
-
parameter_rememberMe: (optional) name of $_POST parameter that activates "remember me" option (value can be 0 or 1). If none, then "remember_me" is implicitly used.
-
logout: (optional) configures logout
-
page: (optional) page that performs logout operation (all requests to this page will pass through this filter). If none, then "logout" is implicitly used.
-
target: (optional) destination page after successful or unsuccessful logout. If none, then "login" is implicitly used.
-
oauth2: (optional) configures authentication via oauth2 provider
-
dao: (mandatory) name of PSR-4 autoload-compliant class (incl. namespace) implementing Authentication\OAuth2\VendorAuthenticationDAO that saves results of authentication in database
-
target: (optional) destination page after successful login. If none, then "index" is implicitly used.
-
login: (optional) generic page where login by provider option is available. If none, then "login" is implicitly used.
-
logout: (optional) page that performs logout operation. If none, then "logout" is implicitly used.
-
authorization: (mandatory) holds a single mechanism to authorize requests (at least one is mandatory!)
-
by_dao: (optional) configures authorization by database
-
page_dao: (mandatory) name of PSR-4 autoload-compliant class (incl. namespace) extending Authorization\DAO\PageAuthorizationDAO that checks user rights in database
-
user_dao: (mandatory) name of PSR-4 autoload-compliant class (incl. namespace) extending Authorization\DAO\UserAuthorizationDAO that checks page rights in database
-
logged_in_callback: (optional) callback page for authenticated users when authorization fails. If none, then "index" is implicitly used.
-
logged_out_callback: (optional) callback page for guest users when authorization fails. If none, then "login" is implicitly used.
-
by_route: (optional) configures authorization by XML, in which case routes tag is required. 1
-
logged_in_callback: (optional) callback page for authenticated users when authorization fails. If none, then "index" is implicitly used.
-
logged_out_callback: (optional) callback page for guest users when authorization fails. If none, then "login" is implicitly used.
For examples of XMLs, check WrapperTest @ unit tests!, (*9)
Notes:
(1) If authorization is by_route, authentication is form with a dao attribute, then class referenced there must also implement Authorization\UserRoles!, (*10)
Users
This tag is required if XML authentication (form tag is present and has no dao attribute) + authorization (by_route tag is present) are used. Syntax is:, (*11)
<users roles="...">
<user id="..." username="..." password="..." roles="..."/>
...
</security>
Where:, (*12)
-
users: (mandatory) holds list of site users, each identified by a user tag
-
roles: (mandatory) holds list of roles guests (non-logged in users) belong to, separated by commas
-
user: (mandatory) holds information about a single user
-
id: (mandatory) holds unique user identifier (eg: 1)
-
username: (optional) holds user's username (eg: john_doe). Mandatory for XML authentication!
-
password: (optional) holds user's password hashed using password_hash (eg: value of
php password_hash("doe", PASSWORD_BCRYPT)
). Mandatory for XML authentication!
-
roles: (optional) holds list of roles user belongs to, separated by commas (eg: USERS, ADMINISTRATORS). Mandatory for XML authentication+authorization
If no user is detected in list above, GUEST role is automatically assumed!, (*13)
Routes
This tag is required if XML authorization (by_route tag is present) is used. Syntax is:, (*14)
<routes roles="...">
<route id="..." roles="..."/>
...
</routes>
Where:, (*15)
-
routes: (mandatory) holds list of site routes, each identified by a route tag
-
roles: (mandatory) holds list of roles all pages are assumed to belong by default to, separated by commas (eg: GUEST)
-
route: (mandatory) holds policies about a specific route
-
id: (mandatory) page relative url (eg: administration)
-
roles: (mandatory) holds list of roles page is associated to, separated by commas (eg: USERS, ADMINISTRATORS)
Binding Points
In order to remain flexible and achieve highest performance, API takes no more assumptions than those absolutely required! It offers developers instead an ability to bind to its prototypes in order to gain certain functionality., (*16)
Declarative Binding
It offers developers an ability to bind declaratively to its prototype classes/interfaces via XML:, (*17)
Programmatic Binding
It offers developers an ability to bind programmatically to its prototypes via Wrapper constructor:, (*18)
Class Prototype |
Ability Gained |
Request |
(mandatory) Collects information about request to be authenticated/authorized. |
Authentication\OAuth2\Driver[] |
(optional) Contains information about OAuth2 vendors to use in OAuth2 authentication later on |
Execution
Once configuration is finished, one can finally use this API to authenticate and authorize by calling Wrapper, which defines following public methods:, (*19)
Method |
Arguments |
Returns |
Description |
__construct |
\SimpleXMLElement $xml, Request $request, Authentication\OAuth2\Driver[] $oauth2Drivers = [] |
void |
Performs authentication and authorization of request based on arguments |
getUserID |
void |
mixed |
Gets logged in user id (integer or string) |
getCsrfToken |
void |
string |
Gets anti-CSRF token to send as "csrf" POST parameter on form login and "state" GET parameter in oauth2 authorization code requests |
getAccessToken |
void |
string |
Gets access token to sign stateless requests with as Bearer HTTP_AUTHORIZATION header (applies if "synchronizer token" or "json web token" persistence is used) |
Both authentication and authorization require following objects to be set beforehand and constructor injected:, (*20)
If authentication/authorization reached a point where request needs to be redirected, constructor throws a SecurityPacket. It may also throw:, (*21)
Handling SecurityPacket
Developers of non-stateless applications are supposed to handle this exception with something like:, (*22)
try {
// sets $xml and $request
$object = new Lucinda\WebSecurity\Wrapper($xml, $request);
// operate with $object to retrieve information
} catch (SecurityPacket $e) {
header("Location: ".$e->getCallback()."?status=".$e->getStatus()."&penalty=".((integer) $e->getTimePenalty()));
exit();
}
Developers of stateless web service applications, however, are supposed to handle this exception with something like:, (*23)
try {
// sets $xml and $request
$object = new Lucinda\WebSecurity\Wrapper($xml, $request);
// use $object to produce a response
} catch (SecurityPacket $e) {
echo json_encode(["status"=>$e->getStatus(), "callback"=>$e->getCallback(), "penalty"=>(integer) $e->getTimePenalty(), "access_token"=>$e->getAccessToken()]);
exit();
// front end will handle above code and make a redirection
}
Handling other exceptions
They can be handled as following:, (*24)
use Lucinda\WebSecurity;
try {
// sets $xml and $request
$object = new Wrapper($xml, $request);
// process $object
} catch (SecurityPacket $e) {
// handle security packet as above
} catch (Authentication\Form\Exception $e) {
// respond with a 400 Bad Request HTTP status (it's either foul play or misconfiguration)
} catch (PersistenceDrivers\Session\HijackException $e) {
// respond with a 400 Bad Request HTTP status (it's always foul play)
} catch (Token\EncryptionException $e) {
// respond with a 400 Bad Request HTTP status (it's always foul play)
} catch (Token\Exception $e) {
// respond with a 400 Bad Request HTTP status (it's either foul play or misconfiguration)
} catch (ConfigurationException $e) {
// show stack trace and exit (it's misconfiguration)
} catch (Authentication\OAuth2\Exception $e) {
// handle as you want (error received from OAuth2 vendor usually from user's decision not to approve your access)
}
Installation
First choose a folder, associate it to a domain then write this command in its folder using console:, (*25)
composer require lucinda/security
Then create a configuration.xml file holding configuration settings (see configuration above) and a index.php file (see getting results above) in project root with following code:, (*26)
$request = new Lucinda\WebSecurity\Request();
$request->setIpAddress($_SERVER["REMOTE_ADDR"]);
$request->setUri($_SERVER["REQUEST_URI"]!="/"?substr($_SERVER["REQUEST_URI"],1):"index");
$request->setMethod($_SERVER["REQUEST_METHOD"]);
$request->setParameters($_POST);
$request->setAccessToken(isset($_SERVER["HTTP_AUTHORIZATION"]) && stripos($_SERVER["HTTP_AUTHORIZATION"], "Bearer ")===0?trim(substr($_SERVER["HTTP_AUTHORIZATION"], 7)):"");
try {
// sets $xml and $request
$object = new Lucinda\WebSecurity\Wrapper(simplexml_load_file("configuration.xml"), $request);
// operate with $object to retrieve information
} catch (Lucinda\WebSecurity\SecurityPacket $e) {
header("Location: ".$e->getCallback()."?status=".$e->getStatus()."&penalty=".((integer) $e->getTimePenalty()));
exit();
}
Then make this file a bootstrap and start developing MVC pattern on top:, (*27)
RewriteEngine on
RewriteRule ^(.*)$ index.php
Unit Tests
For tests and examples, check following files/folders in API sources:, (*28)
Reference Guide
Class SecurityPacket
SecurityPacket class encapsulates an response to an authentication/authorization event that typically requires redirection and defines following methods relevant to developers:, (*29)
Method |
Arguments |
Returns |
Description |
getAccessToken |
void |
string |
Gets access token to sign stateless requests with as Bearer HTTP_AUTHORIZATION header (applies if "synchronizer token" or "json web token" persistence is used) |
getCallback |
void |
integer/string |
Gets URI inside application to redirect to in case of successful/insuccessful authentication or insuccessful authorization |
getStatus |
void |
Authentication\ResultStatus / Authorization\ResultStatus
|
Gets authentication/authorization status (see below) |
getTimePenalty |
void |
integer |
Sets number of seconds client will be banned from authenticating as anti-throttling measure |
Values of getStatus depend on argument enum case:, (*30)
Usage example:, (*31)
https://github.com/aherne/lucinda-framework/blob/master/src/Controllers/SecurityPacket.php, (*32)
Class Request
Request encapsulates information about request necessary for authentication and authorization via following public methods:, (*33)
Method |
Arguments |
Returns |
Description |
setIpAddress |
string $value |
void |
Sets ip address used by client (eg: value of $_SERVER["REMOTE_ADDR"]) |
setContextPath |
string $value |
void |
Sets context path that prefixes page requested by client (eg: prefix of $_SERVER["REQUEST_URI"]) |
setUri |
string $value |
void |
Sets page/resource requested by client without trailing slash (eg: suffix of $_SERVER["REQUEST_URI"]) |
setMethod |
string $value |
void |
Sets HTTP method used by client in page request (eg: value of $_SERVER["REQUEST_METHOD"]) |
setParameters |
array $value |
void |
Sets parameters sent by client as GET/POST along with request (eg: value of $_REQUEST) |
setAccessToken |
string $value |
void |
Sets access token detected from client headers for stateless login (eg: suffix of $_SERVER["HTTP_AUTHORIZATION"]) |
getIpAddress |
string $value |
void |
Gets ip address used by client (eg: value of $_SERVER["REMOTE_ADDR"]) |
getContextPath |
string $value |
void |
Gets context path that prefixes page requested by client (eg: prefix of $_SERVER["REQUEST_URI"]) |
getUri |
string $value |
void |
Gets page/resource requested by client without trailing slash (eg: suffix of $_SERVER["REQUEST_URI"]) |
getMethod |
string $value |
void |
Gets HTTP method used by client in page request (eg: value of $_SERVER["REQUEST_METHOD"]) |
getParameters |
array $value |
void |
Gets parameters sent by client as GET/POST along with request (eg: value of $_REQUEST) |
getAccessToken |
string $value |
void |
Gets access token detected from client headers for stateless login (eg: Bearer value of $_SERVER["HTTP_AUTHORIZATION"]) |
Usage example:, (*34)
https://github.com/aherne/lucinda-framework-engine/blob/master/src/RequestBinder.php, (*35)
Interface OAuth2 Driver
Authentication\OAuth2\Driver interface encapsulates an oauth2 vendor to authenticate with and defines following methods:, (*36)
Method |
Arguments |
Returns |
Description |
getAuthorizationCode |
string $state |
string |
Gets URL to redirect to vendor in order for latter to send back an autorization code |
getAccessToken |
string $authorizationCode |
string |
Asks vendor to exchange authorization code with an access token and returns it |
getUserInformation |
string $accessToken |
Authentication\OAuth2\UserInformation |
Uses access token to get logged in user information from vendor |
getCallbackUrl |
void |
string |
Gets login route of current OAuth2 provider (eg: login/facebook) |
getVendorName |
void |
string |
Gets name of current OAuth2 provider (eg: facebook) |
Usage example:, (*37)
https://github.com/aherne/lucinda-framework-engine/blob/master/src/OAuth2/AbstractSecurityDriver.php, (*38)
Authentication\OAuth2\UserInformation interface contains blueprints for retrieving information about logged in user on OAuth2 provider via following methods:, (*39)
Method |
Arguments |
Returns |
Description |
getEmail |
void |
string |
Gets remote user email |
getId |
void |
integerbr/string |
Gets remote user id |
getName |
void |
string |
Gets remote user name |
Usage example:, (*40)
https://github.com/aherne/lucinda-framework-engine/blob/master/src/OAuth2/AbstractUserInformation.php, (*41)
Interface OAuth2 VendorAuthenticationDAO
Authentication\OAuth2\VendorAuthenticationDAO interface contains blueprints for saving info about logged in user on OAuth2 provider via following methods:, (*42)
Method |
Arguments |
Returns |
Description |
login |
Authentication\OAuth2\UserInformation $userInfo,br/string $vendorName,br/string $accessToken |
string\ |
NULL | Logs in OAuth2 user into current application. Exchanges authenticated OAuth2 user information for a local user ID. |
logout |
mixed $userID |
void |
Logs out local user and removes saved access token |
Usage example:, (*43)
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersOAuth2Authentication.php, (*44)
Interface UserAuthenticationDAO
Authentication\DAO\UserAuthenticationDAO interface contains blueprints for form-based database authentication via following methods:, (*45)
Method |
Arguments |
Returns |
Description |
login |
string $userName,br/string $password |
mixed |
Logs in user in database, returning local user ID or NULL if none found. |
logout |
mixed $userID |
void |
Logs out local user |
Usage example:, (*46)
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersFormAuthentication1.php, (*47)
Abstract Class LoginThrottler
Authentication\Form\LoginThrottler abstract class encapsulates form login throttling algorithm (against brute-force attacks) on a datasource (sql or nosql) via following public methods:, (*48)
Method |
Arguments |
Returns |
Description |
__construct |
Request $request,br/string $userName |
void |
Sets request environment and user about to login, checking throttle status. |
getTimePenalty |
void |
int |
Gets time penalty (in seconds) to apply for found attacker |
setFailure |
void |
void |
Sets current request as failed (attacker detected) |
setSuccess |
void |
void |
Sets current request as normal (no attacker detected) |
Class must be extended in order to implement following abstract protected method:, (*49)
Method |
Arguments |
Returns |
Description |
setCurrentStatus |
void |
int |
Detects current throttling status based on user and request in a database |
Usage example:, (*50)
https://github.com/aherne/lucinda-framework-engine/blob/master/src/AbstractLoginThrottler.php
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/SqlLoginThrottler.php, (*51)
Abstract Class UserAuthorizationDAO
Authorization\DAO\UserAuthorizationDAO abstract class encapsulates database authorization where user accounts are checked in database via following public methods:, (*52)
Method |
Arguments |
Returns |
Description |
__construct |
mixed $userID |
void |
Sets user id to authorize |
getID |
void |
mixed |
Gets user id to authorize |
Class must be extended in order to implement following abstract method:, (*53)
Usage example:, (*54)
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersAuthorization1.php, (*55)
Abstract Class PageAuthorizationDAO
Authorization\DAO\PageAuthorizationDAO abstract class encapsulates database authorization where access control list is checked in database via following public methods:, (*56)
Method |
Arguments |
Returns |
Description |
__construct |
string $pageURL |
void |
Sets requested page to check ACL for |
getID |
void |
int\ |
NULL | Gets database ID of page requested or null if not found |
Class must be extended in order to implement following abstract methods:, (*57)
Method |
Arguments |
Returns |
Description |
isPublic |
void |
bool |
Checks in database if page is accessible by non-logged in users |
detectID |
string $pageURL |
int\ |
NULL | Detects and returns database ID of page requested and returns its value |
Usage example:, (*58)
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/PagesAuthorization.php, (*59)
Interface UserRoles
Authorization\UserRoles interface defines blueprints for any authorization where user roles are checked in database via following public methods:, (*60)
Method |
Arguments |
Returns |
Description |
getRoles |
mixed $userID |
array |
Gets list of roles user belongs to |
Usage example:, (*61)
https://github.com/aherne/lucinda-framework-configurer/blob/master/files/models/dao/UsersFormAuthentication3.php, (*62)