lithium\security\auth\adapter\Form
The Form
adapter provides basic authentication facilities for checking credentials submitted
via a web form against a database. To perform an authentication check, the adapter accepts
an instance of a Request
object which contains the submitted form data in its $data
property.
When a request is submitted, the adapter will take the form data from the Request
object,
apply any filters as appropriate (see the 'filters'
configuration setting below), and
query a model class using using the filtered data. The data is then checked against any
validators configured, which can programmatically check submitted values against database values.
By default, the adapter uses a model called Users
, and lookup fields called 'username'
and
'password'
. These can be customized by setting the 'model'
and 'fields'
configuration keys,
respectively. The 'model'
key accepts either a model name (i.e. Customers
), or a
fully-namespaced model class name (i.e. my_app\models\Customers
). The 'fields'
setting
accepts an array of field names to use when looking up a user. An example configuration,
including a custom model class and lookup fields might look like the following:
Auth::config([
'customer' => [
'adapter' => 'Form',
'model' => 'Customers',
'fields' => ['email', 'password']
]
]);
If the field names present in the form match the fields used in the database lookup, the above
will suffice. If, however, the form fields must be matched to different database field names,
you can specify an array which matches up the form field names to their corresponding database
field names. Suppose, for example, user authentication information in a MongoDB database is
nested within a sub-object called login
. The adapter could be configured as follows:
Auth::config([
'customer' => [
'adapter' => 'Form',
'model' => 'Customers',
'fields' => ['username' => 'login.username', 'password' => 'login.password'],
'scope' => ['active' => true]
]
]);
Note that any additional fields may be specified which should be included in the query. For
example, if a user must select a group when logging in, you may override the 'fields'
key with
that value as well (i.e. 'fields' => ['username', 'password', 'group']
). If a field is
specified which is not present in the request data, the value in the authentication query will be
null
). Note that this will only submit data that is specified in the incoming request. If you
would like to further limit the query using fixed conditions, use the 'scope'
key, as shown in
the example above.
Pre-Query Filtering
As mentioned, prior to any queries being executed, the request data is modified by any filters
configured. Filters are callbacks which accept the value of a submitted form field as input, and
return a modified version of the value as output. Filters can be any PHP callable, i.e. a closure
or array('ClassName', 'method')
.
For example, if you're doing simple password hashing against a legacy application, you can configure the adapter as follows:
Auth::config([
'default' => [
'adapter' => 'Form',
'filters' => ['password' => ['lithium\security\Hash', 'calculate']],
'validators' => ['password' => false]
]
]);
This applies the default system hash (SHA 512) against the password prior to using it in the
query, and overrides 'validators'
to disable the default crypto-based query validation that
would occur after the query.
Note that if you are specifying the 'fields'
configuration using key / value pairs, the key
used to specify the filter must match the key side of the 'fields'
assignment. Additionally,
specifying a filter with no key allows the entire data array to be filtered, as in the following:
Auth::config([
'default' => [
'adapter' => 'Form',
'filters' => [function ($data) {
// Make any modifications to $data, including adding/removing keys
return $data;
}]
]
]);
For more information, see the _filters()
method or the $_filters
property.
Post-Query Validation
In addition to filtering data, you can also apply validators to do check submitted form data against database values programmatically. For example, the default adapter uses a cryptographic hash function which operates in constant time to validate passwords. Configuring this validator manually would work as follows:
use lithium\security\Password;
Auth::config([
'default' => [
'adapter' => 'Form',
'validators' => [
'password' => function($form, $data) {
return Password::check($form, $data);
}
]
]
]);
As with filters, each validator can be defined as any PHP callable, and must be keyed using the
name of the form field submitted (if form and database field names do not match). If a validator
is specified with no key, it will apply to all data submitted. See the $_validators
property
and the _validate()
method for more information.
Source
class Form extends \lithium\core\ObjectDeprecated {
/**
* The name of the model class to query against. This can either be a model name (i.e.
* `'Users'`), or a fully-namespaced class reference (i.e. `'app\models\Users'`). When
* authenticating users, the magic `first()` method is invoked against the model to return the
* first record found when combining the conditions in the `$_scope` property with the
* authentication data yielded from the `Request` object in `Form::check()`. (Note that the
* model method called is configurable using the `$_query` property).
*
* @see lithium\security\auth\adapter\Form::$_query
* @var string
*/
protected $_model = '';
/**
* The list of fields to extract from the `Request` object and use when querying the database.
* This can either be a simple array of field names, or a set of key/value pairs, which map
* the field names in the request to database field names.
*
* For example, if you had a form field name `username`, which mapped to a database field named
* username, you could use the following in the `'fields'` configuration:
*
* ``` embed:lithium\tests\cases\security\auth\adapter\FormTest::testMixedFieldMapping(3-3) ```
*
* This is especially relevant for document databases, where you may want to map a form field to
* a nested document field:
*
* ```
* 'fields' => ['username' => 'login.username', 'password'],
* ```
*
* @var array
*/
protected $_fields = [];
/**
* Additional data to apply to the model query conditions when looking up users, i.e.
* `array('active' => true)` to disallow authenticating against inactive user accounts.
*
* @var array
*/
protected $_scope = [];
/**
* Callback filters to apply to request data before using it in the authentication query. Each
* key in the array must match a request field specified in the `$_fields` property, and each
* value must either be a reference to a function or method name, or a closure. For example, to
* automatically hash passwords using simple SHA 512 hashing, the `Form` adapter could be
* configured with the following:
* ```
* ['password' => ['lithium\security\Hash', 'calculate']]
* ```
*
* Optionally, you can specify a callback with no key, which will receive (and can modify) the
* entire credentials array before the query is executed, as in the following example:
*
* ```
* Auth::config([
* 'members' => [
* 'adapter' => 'Form',
* 'model' => 'Member',
* 'fields' => ['email', 'password'],
* 'filters' => [function($data) {
* // If the user is outside the company, then their account must have the
* // 'private' field set to true in order to log in:
* if (!preg_match('/@mycompany\.com$/', $data['email'])) {
* $data['private'] = true;
* }
* return $data;
* }]
* ]
* ]);
* ```
*
* @see lithium\security\auth\adapter\Form::$_fields
* @var array
*/
protected $_filters = [];
/**
* An array of callbacks, keyed by form field name, which make an assertion about a piece of
* submitted form data. Each validator should accept the value of the form field submitted
* (which will be modified by any applied filters), and return a boolean value indicating the
* success of the validation. If a validator is specified with no key, it will receive all form
* data and all database data. See the `_validate()` method for more information.
*
* @see lithium\security\auth\adapter\Form::_validate()
* @var array
*/
protected $_validators = [];
/**
* If you require custom model logic in your authentication query, use this setting to specify
* which model method to call, and this method will receive the authentication query. In return,
* the `Form` adapter expects a `Record` object which implements the `data()` method. See the
* constructor for more information on setting this property. Defaults to `'first'`, which
* calls, for example, `Users::first()`.
*
* @see lithium\security\auth\adapter\Form::__construct()
* @see lithium\data\entity\Record::data()
* @var string
*/
protected $_query = 'first';
/**
* List of configuration properties to automatically assign to the properties of the adapter
* when the class is constructed.
*
* @var array
*/
protected $_autoConfig = ['model', 'fields', 'scope', 'filters', 'validators', 'query'];
/**
* Constructor. Sets the initial configuration for the `Form` adapter, as detailed below.
*
* @see lithium\security\auth\adapter\Form::$_model
* @see lithium\security\auth\adapter\Form::$_fields
* @see lithium\security\auth\adapter\Form::$_filters
* @see lithium\security\auth\adapter\Form::$_validators
* @see lithium\security\auth\adapter\Form::$_query
* @param array $config Available configuration options:
* `'model'` _string_: The name of the model class to use. See the `$_model`
* property for details.
* `'fields'` _array_: The model fields to query against when taking input from
* the request data. See the `$_fields` property for details.
* `'scope'` _array_: Any additional conditions used to constrain the
* authentication query. For example, if active accounts in an application have
* an `active` field which must be set to `true`, you can specify
* `'scope' => ['active' => true]`. See the `$_scope` property for more
* details.
* `'filters'` _array_: Named callbacks to apply to request data before the user
* lookup query is generated. See the `$_filters` property for more details.
* `'validators'` _array_: Named callbacks to apply to fields in request data and
* corresponding fields in database data in order to do programmatic
* authentication checks after the query has occurred. See the `$_validators`
* property for more details.
* `'query'` _string_: Determines the model method to invoke for authentication
* checks. See the `$_query` property for more details.
* @return void
*/
public function __construct(array $config = []) {
$defaults = [
'model' => 'Users',
'query' => 'first',
'filters' => [],
'validators' => [],
'fields' => ['username', 'password']
];
$config += $defaults;
$password = function($form, $data) {
return Password::check($form, $data);
};
$config['validators'] = array_filter($config['validators'] + compact('password'));
parent::__construct($config + $defaults);
}
/**
* Initializes values configured in the constructor.
*
* @return void
*/
protected function _init() {
parent::_init();
foreach ($this->_fields as $key => $val) {
if (is_int($key)) {
unset($this->_fields[$key]);
$this->_fields[$val] = $val;
}
}
if (!class_exists($model = Libraries::locate('models', $this->_model))) {
throw new ClassNotFoundException("Model class '{$this->_model}' not found.");
}
$this->_model = $model;
}
/**
* Called by the `Auth` class to run an authentication check against a model class using the
* credentials in a data container (a `Request` object), and returns an array of user
* information on success, or `false` on failure.
*
* @param object $credentials A data container which wraps the authentication credentials used
* to query the model (usually a `Request` object). See the documentation for this
* class for further details.
* @param array $options Additional configuration options. Not currently implemented in this
* adapter.
* @return array Returns an array containing user information on success, or `false` on failure.
*/
public function check($credentials, array $options = []) {
$model = $this->_model;
$query = $this->_query;
$data = $this->_filters($this->_data($credentials->data));
$validate = array_flip(array_intersect_key($this->_fields, $this->_validators));
$conditions = $this->_scope + array_diff_key($data, $validate);
$user = $model::$query(compact('conditions') + $options);
if (!$user) {
return false;
}
return $this->_validate($user, $data);
}
/**
* A pass-through method called by `Auth`. Returns the value of `$data`, which is written to
* a user's session. When implementing a custom adapter, this method may be used to modify or
* reject data before it is written to the session.
*
* @param array $data User data to be written to the session.
* @param array $options Adapter-specific options. Not implemented in the `Form` adapter.
* @return array Returns the value of `$data`.
*/
public function set($data, array $options = []) {
return $data;
}
/**
* Called by `Auth` when a user session is terminated. Not implemented in the `Form` adapter.
*
* @param array $options Adapter-specific options. Not implemented in the `Form` adapter.
* @return void
*/
public function clear(array $options = []) {}
/**
* Iterates over the filters configured in `$_filters` which are applied to submitted form data
* _before_ it is used in the query.
*
* @see lithium\security\auth\adapter\Form::$_filters
* @param array $data The array of raw form data to be filtered.
* @return array Callback result.
*/
protected function _filters($data) {
$result = [];
foreach ($this->_fields as $from => $to) {
$result[$to] = isset($data[$from]) ? $data[$from] : null;
if (!isset($this->_filters[$from])) {
$result[$to] = is_scalar($result[$to]) ? $result[$to] : null;
continue;
}
if ($this->_filters[$from] === false) {
continue;
}
if (!is_callable($this->_filters[$from])) {
$message = "Authentication filter for `{$from}` is not callable.";
throw new UnexpectedValueException($message);
}
$result[$to] = call_user_func($this->_filters[$from], $result[$to]);
}
if (!isset($this->_filters[0])) {
return $result;
}
if (!is_callable($this->_filters[0])) {
throw new UnexpectedValueException("Authentication filter is not callable.");
}
return call_user_func($this->_filters[0], $result);
}
/**
* After an authentication query against the configured model class has occurred, this method
* iterates over the configured validators and checks each one by passing the submitted form
* value as the first parameter, and the corresponding database value as the second. The
* validator then returns a boolean to indicate success. If the validator fails, it will cause
* the entire authentication operation to fail. Note that any filters applied to a form field
* will affect the data passed to the validator.
*
* @see lithium\security\auth\adapter\Form::__construct()
* @see lithium\security\auth\adapter\Form::$_validators
* @param object $user The user object returned from the database. This object must support a
* `data()` method, which returns the object's array representation, and
* also returns individual field values by name.
* @param array $data The form data submitted in the request and passed to `Form::check()`.
* @return array Returns an array of authenticated user data on success, otherwise `false` if
* any of the configured validators fails. See `'validators'` in the `$config`
* parameter of `__construct()`.
*/
protected function _validate($user, array $data) {
foreach ($this->_validators as $field => $validator) {
if (!isset($this->_fields[$field]) || $field === 0) {
continue;
}
if (!is_callable($validator)) {
$message = "Authentication validator for `{$field}` is not callable.";
throw new UnexpectedValueException($message);
}
$field = $this->_fields[$field];
$value = isset($data[$field]) ? $data[$field] : null;
if (!call_user_func($validator, $value, $user->data($field))) {
return false;
}
}
$user = $user->data();
if (!isset($this->_validators[0])) {
return $user;
}
if (!is_callable($this->_validators[0])) {
throw new UnexpectedValueException("Authentication validator is not callable.");
}
return call_user_func($this->_validators[0], $data, $user) ? $user : false;
}
/**
* Checks if the data container values are inside indexed arrays from binding.
* Get the values from the binding coresponding to the model if such exists.
*
* @see lithium\security\auth\adapter\Form::check
* @param array $data The array of raw form data.
* @return array Original or sub array of the form data.
*/
protected function _data($data) {
$model = $this->_model;
$index = strtolower(Inflector::singularize($model::meta('name')));
return isset($data[$index]) && is_array($data[$index]) ? $data[$index] : $data;
}
}