Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to configure validator with custom authentication result codes and validation messages #47

Merged
merged 6 commits into from May 14, 2019
@@ -13,6 +13,7 @@ The available configuration options include:
- `identity`: the identity or name of the identity field in the provided context.
- `credential`: credential or the name of the credential field in the provided context.
- `service`: an instance of `Zend\Authentication\AuthenticationService`.
- `code_map`: map of authentication attempt result codes to validator message keys.

## Usage

@@ -33,3 +34,31 @@ $validator->isValid('myIdentity', [
'myCredentialContext' => 'myCredential',
]);
```

## Configuring custom authentication result codes and messages

Constructor configuration option `code_map` is a map of custom authentication result
This conversation was marked as resolved by weierophinney

This comment has been minimized.

Copy link
@weierophinney

weierophinney May 13, 2019

Member
Suggested change
Constructor configuration option `code_map` is a map of custom authentication result
The constructor configuration option `code_map` is a map of custom authentication result
codes to validation messages keys.

`code_map` can specify custom validation message key. New message template
will be registered for that key, which can further be customized
using `Validator::setMessage()` method or `messages` configuration option.
This conversation was marked as resolved by weierophinney

This comment has been minimized.

Copy link
@weierophinney

weierophinney May 13, 2019

Member

Let's try the following instead:

A `code_map` value can map to a custom validation message key if desired. If you do, you should also register a message template for that key via either the `messages` configuration option or the `AuthenticationValidator::setMessage()` method:

This comment has been minimized.

Copy link
@Xerkus

Xerkus May 13, 2019

Author Member

Setting message template is not required, it defaults to template for general error, same way as when no custom mapping is provided.

This comment has been minimized.

Copy link
@weierophinney

weierophinney May 13, 2019

Member

So make that explicit in the documentation, so somebody not familiar with how validators work has everything they need to understand it.

This comment has been minimized.

Copy link
@Xerkus

Xerkus May 13, 2019

Author Member

AbstractValidator::setMessage() can only be invoked for existing message key provided by validator

        if (! isset($this->abstractOptions['messageTemplates'][$messageKey])) {
            throw new Exception\InvalidArgumentException("No message template exists for key '$messageKey'");
        }

What custom message key in code_map does is adds new entry to messageTemplates.
After that messages option or setMessage() can be used with that key


```php
use Zend\Authentication\Validator\Authentication as AuthenticationValidator;
$validator = new AuthenticationValidator([
'code_map' => [
// map custom result code to existing message
-990 => AuthenticationValidator::IDENTITY_NOT_FOUND,
// map custom result code to a new message type
-991 => 'custom_error_message_key',
],
'messages' => [
// provide message template for custom message type defined above
'custom_error_message_key' => 'Custom Error Happened'
],
]);
$validator->setMessage('Custom Error Happened', 'custom_error_message_key');
```
@@ -1,7 +1,7 @@
<?php
/**
* @see https://github.com/zendframework/zend-authentication for the canonical source repository
* @copyright Copyright (c) 2013-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @copyright Copyright (c) 2013-2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-authentication/blob/master/LICENSE.md New BSD License
*/
@@ -15,6 +15,9 @@
use Zend\Stdlib\ArrayUtils;
use Zend\Validator\AbstractValidator;
use function is_array;
use function is_string;
/**
* Authentication Validator
*/
@@ -41,6 +44,12 @@ class Authentication extends AbstractValidator
Result::FAILURE_UNCATEGORIZED => self::UNCATEGORIZED,
];
/**
* Authentication\Result codes mapping configurable overrides
* @var string[]
*/
protected $codeMap = [];
/**
* Error Messages
* @var array
@@ -89,18 +98,31 @@ public function __construct($options = null)
}
if (is_array($options)) {
if (array_key_exists('adapter', $options)) {
if (isset($options['adapter'])) {
$this->setAdapter($options['adapter']);
}
if (array_key_exists('identity', $options)) {
if (isset($options['identity'])) {
$this->setIdentity($options['identity']);
}
if (array_key_exists('credential', $options)) {
if (isset($options['credential'])) {
$this->setCredential($options['credential']);
}
if (array_key_exists('service', $options)) {
if (isset($options['service'])) {
$this->setService($options['service']);
}
if (isset($options['code_map'])) {
foreach ($options['code_map'] as $code => $template) {
if (empty($template) || ! is_string($template)) {
throw new Exception\InvalidArgumentException(
'Message key in code_map option must be a non-empty string'
);
}
if (! isset($this->messageTemplates[$template])) {
$this->messageTemplates[$template] = $this->messageTemplates[static::GENERAL];
}
$this->codeMap[(int) $code] = $template;
This conversation was marked as resolved by weierophinney

This comment has been minimized.

Copy link
@weierophinney

weierophinney May 13, 2019

Member

Just a general question: why are the codes restricted to integers? I know we've run into problems in zend-db around this, as some extensions throw exceptions that use non-integer codes.

This comment has been minimized.

Copy link
@Xerkus

Xerkus May 13, 2019

Author Member

I am not sure what was the reason for it, but it is what Result uses:

$this->code = (int) $code;

This comment has been minimized.

Copy link
@weierophinney

weierophinney May 13, 2019

Member

That's rationale enough for me - in fact, that may have been due to us running into those non-standard codes. I just wanted to make sure that there was a solid reason for it.

It might be good to indicate in the docs section that the code map maps result codes to validation error codes. I wasn't clear on that, and this whole question could have gone away if it were documented. :)

}
}
}
parent::__construct($options);
}
@@ -244,15 +266,27 @@ public function isValid($value = null, $context = null)
return true;
}
$code = self::GENERAL;
if (array_key_exists($result->getCode(), self::CODE_MAP)) {
$code = self::CODE_MAP[$result->getCode()];
}
$this->error($code);
$messageKey = $this->mapResultCodeToMessageKey($result->getCode());
$this->error($messageKey);
return false;
}
/**
* @param int $code Authentication result code
* @return string Message key that should be used for the code
*/
protected function mapResultCodeToMessageKey($code)
{
if (isset($this->codeMap[$code])) {
return $this->codeMap[$code];
}
if (array_key_exists($code, static::CODE_MAP)) {
return static::CODE_MAP[$code];
}
return self::GENERAL;
}
/**
* @return ValidatableAdapterInterface
* @throws Exception\RuntimeException if no adapter present in
@@ -1,14 +1,13 @@
<?php
/**
* @see https://github.com/zendframework/zend-authentication for the canonical source repository
* @copyright Copyright (c) 2013-2018 Zend Technologies USA Inc. (https://www.zend.com)
* @copyright Copyright (c) 2013-2019 Zend Technologies USA Inc. (https://www.zend.com)
* @license https://github.com/zendframework/zend-authentication/blob/master/LICENSE.md New BSD License
*/
namespace ZendTest\Authentication\Validator;
use PHPUnit\Framework\TestCase;
use RuntimeException;
use Zend\Authentication\Adapter\ValidatableAdapterInterface;
use Zend\Authentication\AuthenticationService;
use Zend\Authentication\Exception;
@@ -54,6 +53,124 @@ public function testOptions()
$this->assertSame($auth->getCredential(), 'password');
}
public function testConstructorOptionCodeMapOverridesDefaultMap()
{
$authAdapter = new AuthTest\TestAsset\ValidatableAdapter(AuthenticationResult::FAILURE_UNCATEGORIZED);
$auth = new AuthenticationValidator([
'adapter' => $authAdapter,
'service' => $this->authService,
'identity' => 'username',
'credential' => 'password',
'code_map' => [
AuthenticationResult::FAILURE_UNCATEGORIZED => AuthenticationValidator::IDENTITY_NOT_FOUND,
]
]);
$this->assertFalse($auth->isValid());
$this->assertArrayHasKey(
AuthenticationValidator::IDENTITY_NOT_FOUND,
$auth->getMessages(),
print_r($auth->getMessages(), true)
);
}
public function testConstructorOptionCodeMapUsesDefaultMapForOmittedCodes()
{
$authAdapter = new AuthTest\TestAsset\ValidatableAdapter(AuthenticationResult::FAILURE_IDENTITY_AMBIGUOUS);
$auth = new AuthenticationValidator([
'adapter' => $authAdapter,
'service' => $this->authService,
'identity' => 'username',
'credential' => 'password',
'code_map' => [
AuthenticationResult::FAILURE_UNCATEGORIZED => AuthenticationValidator::IDENTITY_NOT_FOUND,
]
]);
$this->assertFalse($auth->isValid());
$this->assertArrayHasKey(
AuthenticationValidator::IDENTITY_AMBIGUOUS,
$auth->getMessages(),
print_r($auth->getMessages(), true)
);
}
public function testCodeMapAllowsToSpecifyCustomCodes()
{
$authAdapter = new AuthTest\TestAsset\ValidatableAdapter(-999);
$auth = new AuthenticationValidator([
'adapter' => $authAdapter,
'service' => $this->authService,
'identity' => 'username',
'credential' => 'password',
'code_map' => [
-999 => AuthenticationValidator::IDENTITY_NOT_FOUND,
]
]);
$this->assertFalse($auth->isValid());
$this->assertArrayHasKey(
AuthenticationValidator::IDENTITY_NOT_FOUND,
$auth->getMessages(),
print_r($auth->getMessages(), true)
);
}
public function testCodeMapAllowsToAddCustomMessageTemplates()
{
$auth = new AuthenticationValidator([
'code_map' => [
-999 => 'custom_error',
]
]);
$templates = $auth->getMessageTemplates();
$this->assertArrayHasKey(
'custom_error',
$templates,
print_r($templates, true)
);
}
/**
* @depends testCodeMapAllowsToAddCustomMessageTemplates
*/
public function testCodeMapCustomMessageTemplateValueDefaultsToGeneralMessageTemplate()
{
$auth = new AuthenticationValidator([
'code_map' => [
-999 => 'custom_error',
]
]);
$templates = $auth->getMessageTemplates();
$this->assertEquals($templates['general'], $templates['custom_error']);
}
/**
* @depends testCodeMapAllowsToAddCustomMessageTemplates
*/
public function testCustomMessageTemplateValueCanBeProvidedAsOption()
{
$auth = new AuthenticationValidator([
'code_map' => [
-999 => 'custom_error',
],
'messages' => [
'custom_error' => 'Custom Error'
]
]);
$templates = $auth->getMessageTemplates();
$this->assertEquals('Custom Error', $templates['custom_error']);
}
public function testCodeMapOptionRequiresMessageKeyToBeString()
{
$this->expectException(Exception\InvalidArgumentException::class);
$this->expectExceptionMessage('Message key in code_map option must be a non-empty string');
$auth = new AuthenticationValidator([
'code_map' => [
-999 => [],
]
]);
}
public function testSetters()
{
$this->validator->setAdapter($this->authAdapter);
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.