Compare commits

..

16 Commits

Author SHA1 Message Date
0b7958ddb9
add product logo 2023-06-09 09:32:15 +02:00
e4e27c8952
adjust version informations 2023-05-26 09:37:10 +02:00
20bb53c215
make installable in OXID 6.5.2 2023-05-26 09:33:54 +02:00
d61dcda7fb
adjust version informations 2023-02-18 22:07:52 +01:00
08e58b1b88
define compatible TOTP plugin version 2023-02-18 22:05:52 +01:00
dec4b3cebd
rename from security key to login key 2023-02-17 09:21:52 +01:00
c63724c064
improve assertions 2023-02-17 09:21:51 +01:00
018e91bc0c
add any further debug loggings 2023-02-16 10:31:53 +01:00
10d8fddd88
add wishlist 2023-02-16 10:31:52 +01:00
cc0e0fb32b
update changelog 2023-02-15 22:33:56 +01:00
291c99e4e5
convert the user handle to an arrayBuffer
because of format errors in case of given handle (usually concerns platform authenticators only)
2023-02-14 16:27:29 +01:00
462bb34447
add migrations 2023-02-11 23:20:55 +01:00
028fc05d54
add missing integration test 2023-02-06 22:39:23 +01:00
e11b93e300
assert some expectations 2023-02-06 22:38:03 +01:00
e72f365a29
adjust tests 2023-02-05 23:45:16 +01:00
161787d26f
assert valid credential response 2023-02-05 22:50:19 +01:00
39 changed files with 910 additions and 372 deletions

View File

@ -4,13 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased](https://git.d3data.de/D3Public/webauthn/compare/2.0.0.1...rel_2.x) ## [Unreleased](https://git.d3data.de/D3Public/webauthn/compare/1.0.0.0...rel_1.x)
## [1.0.0.0](https://git.d3data.de/D3Public/webauthn/releases/tag/1.0.0.0) - 2019-08-19 ## [1.0.0.0](https://git.d3data.de/D3Public/webauthn/compare/0.1.0.0...1.0.0.0) - 2023-05-25
### Added ### Added
- 2-factor authentication for logins in front- and backend in addition to username and password - make installable in OXID 6.5.2
- Activation and setup possible in the front and back end
- Authentication is shown for user accounts that have this enabled - otherwise the usual default login. ## [0.1.0.0](https://git.d3data.de/D3Public/webauthn/releases/tag/0.1.0.0) - 2023-02-18
- Access can be set up in the Auth app by scannable QR code or copyable character string ### Added
- Validation of one-time passwords and generation of QR codes are only carried out within the shop - no communication to the outside necessary - Key management in front and back end
- static backup codes also allow (limited) login without access to the generation tool - FIDO2 / passkey as password alternative for login in front- and backend - password as fallback
- compatible with our 2FA one-time password module (https://packagist.org/packages/d3/oxid-twofactor-onetimepassword from version 2.1.0.0)

View File

@ -1,13 +1,15 @@
[![deutsche Version](https://logos.oxidmodule.com/de2_xs.svg)](README.md) [![deutsche Version](https://logos.oxidmodule.com/de2_xs.svg)](README.md)
[![english version](https://logos.oxidmodule.com/en2_xs.svg)](README.en.md) [![english version](https://logos.oxidmodule.com/en2_xs.svg)](README.en.md)
# DÂł Passwordless login for OXID eShop # Passwordless login for OXID eShop
With this module, the login in the OXID shop can be carried out with a hardware token instead of a password (WebAuthn / FIDO2 based). ![Passwordless login for OXID eShop](src/logo.png)
With this module, the login in the OXID shop can be carried out with a hardware based login key (WebAuthn / FIDO2 based passkey) instead of a password.
This secures the login in the frontend and (if allowed for the user) also in the backend. This secures the login in the frontend and (if allowed for the user) also in the backend.
Security keys are devices that contain cryptographic keys. These can be used for two-factor authentication. The security key must support the standard "[WebAuthn](https://w3c.github.io/webauthn/#webauthn-authenticator)". Login keys are from devices that contain cryptographic keys. These can be used for two-factor authentication. The login key device must support the standard "[WebAuthn](https://w3c.github.io/webauthn/#webauthn-authenticator)".
The key management is done in the admin area and in the user's "My Account". The key management is done in the admin area and in the user's "My Account".
@ -63,12 +65,14 @@ and its requirements.
The Flow and Wave themes are supported by default. Other themes may require customisation. The Flow and Wave themes are supported by default. Other themes may require customisation.
## Module installation ## Module installation / update
Open a command line interface and navigate to the shop root directory (parent of source and vendor). Execute the following command. Adapt the paths to your environment. Open a command line interface and navigate to the shop root directory (parent of source and vendor). Execute the following commands. Adapt the paths to your environment.
```bash ```bash
php composer require d3/oxid-twofactor-passwordless:^1.0 composer require d3/oxid-twofactor-passwordless:^1.0
./vendor/bin/oe-eshop-db_migrate migrations:migrate d3webauthn
``` ```
If a reference to an unsuitable package `symfony/process` is shown, this must be changed. To do this, please add the switch `-W` to the above command (`... require -W ...`). If a reference to an unsuitable package `symfony/process` is shown, this must be changed. To do this, please add the switch `-W` to the above command (`... require -W ...`).

View File

@ -1,13 +1,15 @@
[![deutsche Version](https://logos.oxidmodule.com/de2_xs.svg)](README.md) [![deutsche Version](https://logos.oxidmodule.com/de2_xs.svg)](README.md)
[![english version](https://logos.oxidmodule.com/en2_xs.svg)](README.en.md) [![english version](https://logos.oxidmodule.com/en2_xs.svg)](README.en.md)
# DÂł Passwortloses Anmelden fĂĽr OXID eShop # Passwortloses Anmelden fĂĽr OXID eShop
Mit diesem Modul kann die Anmeldung im OXID-Shop mit einem Hardwaretoken anstelle eines Passworts durchgefĂĽhrt werden (WebAuthn / FIDO2 basiert). ![Passwortloses Anmelden fĂĽr OXID eShop](src/logo.png)
Mit diesem Modul kann die Anmeldung im OXID-Shop mit einem hardwarebasierten AnmeldeschlĂĽssel (WebAuthn / FIDO2 basierter passkey) anstelle eines Passworts durchgefĂĽhrt werden.
Hierbei wird die Anmeldung im Frontend und (sofern fĂĽr den Benutzer erlaubt) auch im Backend gesichert. Hierbei wird die Anmeldung im Frontend und (sofern fĂĽr den Benutzer erlaubt) auch im Backend gesichert.
Sicherheitsschlüssel sind Geräte, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Der Sicherheitsschlüssel muss den Standard "[WebAuthn](https://w3c.github.io/webauthn/#webauthn-authenticator)" unterstützen. Anmeldeschlüssel stammen von Geräten, die kryptografische Schlüssel beeinhalten. Diese können für die Zwei-Faktor-Authentifizierung verwendet werden. Das Anmeldeschlüsselgerät muss den Standard "[WebAuthn](https://w3c.github.io/webauthn/#webauthn-authenticator)" unterstützen.
Die SchlĂĽsselverwaltung erfolgt im Adminbereich sowie im "Mein Konto" des Benutzers. Die SchlĂĽsselverwaltung erfolgt im Adminbereich sowie im "Mein Konto" des Benutzers.
@ -63,12 +65,14 @@ und dessen Anforderungen.
Im Standard wird das Flow- und Wave-Theme unterstützt. Andere Themes können Anpassungen erfordern. Im Standard wird das Flow- und Wave-Theme unterstützt. Andere Themes können Anpassungen erfordern.
## Modulinstallation ## Modulinstallation / -update
Ă–ffnen Sie eine Kommandozeile und navigieren Sie zum Stammverzeichnis des Shops (Elternverzeichnis von source und vendor). FĂĽhren Sie den folgenden Befehl aus. Passen Sie die Pfadangaben an Ihre Installationsumgebung an. Ă–ffnen Sie eine Kommandozeile und navigieren Sie zum Stammverzeichnis des Shops (Elternverzeichnis von source und vendor). FĂĽhren Sie die folgenden Befehle aus. Passen Sie die Pfadangaben an Ihre Installationsumgebung an.
```bash ```bash
php composer require d3/oxid-twofactor-passwordless:^1.0 composer require d3/oxid-twofactor-passwordless:^1.0
./vendor/bin/oe-eshop-db_migrate migrations:migrate d3webauthn
``` ```
Wird ein Hinweis auf ein unpassendes Paket "symfony/process" gezeigt, muss dieses geändert werden. Fügen Sie dazu in den oben genannten Befehl bitte den Schalter `-W` ein (`... require -W ...`). Wird ein Hinweis auf ein unpassendes Paket "symfony/process" gezeigt, muss dieses geändert werden. Fügen Sie dazu in den oben genannten Befehl bitte den Schalter `-W` ein (`... require -W ...`).

View File

@ -18,7 +18,9 @@
"token", "token",
"yubikey", "yubikey",
"solokey", "solokey",
"credential" "credential",
"login",
"passkey"
], ],
"authors": [ "authors": [
{ {
@ -40,8 +42,8 @@
}, },
"require": { "require": {
"php": ">=7.4", "php": ">=7.4",
"oxid-esales/oxideshop-ce": "6.8 - 6.13", "oxid-esales/oxideshop-ce": "6.8 - 6.14",
"web-auth/webauthn-lib": "^4.3", "web-auth/webauthn-lib": "^3.3",
"nyholm/psr7": "^1.5.1", "nyholm/psr7": "^1.5.1",
"nyholm/psr7-server": "^1.0.2", "nyholm/psr7-server": "^1.0.2",
"ext-json": "*", "ext-json": "*",
@ -55,7 +57,8 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"D3\\Webauthn\\": "../../../source/modules/d3/oxwebauthn" "D3\\Webauthn\\": "../../../source/modules/d3/oxwebauthn",
"D3\\Webauthn\\Migrations\\": "../../../source/modules/d3/oxwebauthn/migration/data"
} }
}, },
"suggest": { "suggest": {

View File

@ -15,7 +15,9 @@ declare(strict_types=1);
namespace D3\Webauthn\Application\Controller\Admin; namespace D3\Webauthn\Application\Controller\Admin;
use Assert\Assert;
use Assert\AssertionFailedException; use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredential; use D3\Webauthn\Application\Model\Credential\PublicKeyCredential;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList; use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList;
@ -83,7 +85,7 @@ class d3user_webauthn extends AdminDetailsController
try { try {
$this->setPageType('requestnew'); $this->setPageType('requestnew');
$this->setAuthnRegister(); $this->setAuthnRegister();
} catch (Exception|ContainerExceptionInterface|NotFoundExceptionInterface|DoctrineDriverException $e) { } catch (AssertionFailedException|ContainerExceptionInterface|NotFoundExceptionInterface|DoctrineDriverException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e->getMessage()); d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e->getMessage());
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getMessage(), ['UserId' => $this->getEditObjectId()]); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getMessage(), ['UserId' => $this->getEditObjectId()]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString()); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
@ -107,12 +109,15 @@ class d3user_webauthn extends AdminDetailsController
} }
$credential = Registry::getRequest()->getRequestEscapedParameter('credential'); $credential = Registry::getRequest()->getRequestEscapedParameter('credential');
if (strlen((string) $credential)) { Assert::that($credential)->minLength(1, 'Credential should not be empty.');
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($credential);
/** @var Webauthn $webauthn */ $keyname = Registry::getRequest()->getRequestEscapedParameter('keyname');
$webauthn = d3GetOxidDIC()->get(Webauthn::class); Assert::that($keyname)->minLength(1, 'Key name should not be empty.');
$webauthn->saveAuthn($credential, Registry::getRequest()->getRequestEscapedParameter('keyname'));
} d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($credential);
/** @var Webauthn $webauthn */
$webauthn = d3GetOxidDIC()->get(Webauthn::class);
$webauthn->saveAuthn($credential, $keyname);
} catch (WebauthnException $e) { } catch (WebauthnException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId' => $this->getEditObjectId()]); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId' => $this->getEditObjectId()]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString()); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
@ -136,11 +141,14 @@ class d3user_webauthn extends AdminDetailsController
/** /**
* @throws DoctrineDriverException * @throws DoctrineDriverException
* @throws DoctrineException * @throws DoctrineException
* @throws InvalidArgumentException
*/ */
public function setAuthnRegister(): void public function setAuthnRegister(): void
{ {
/** @var Webauthn $authn */
$authn = d3GetOxidDIC()->get(Webauthn::class); $authn = d3GetOxidDIC()->get(Webauthn::class);
/** @var User $user */
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$user->load($this->getEditObjectId()); $user->load($this->getEditObjectId());
$publicKeyCredentialCreationOptions = $authn->getCreationOptions($user); $publicKeyCredentialCreationOptions = $authn->getCreationOptions($user);
@ -165,6 +173,7 @@ class d3user_webauthn extends AdminDetailsController
*/ */
public function getCredentialList($userId): array public function getCredentialList($userId): array
{ {
/** @var User $oUser */
$oUser = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); $oUser = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$oUser->load($userId); $oUser->load($userId);

View File

@ -15,6 +15,9 @@ declare(strict_types=1);
namespace D3\Webauthn\Application\Controller\Admin; namespace D3\Webauthn\Application\Controller\Admin;
use Assert\Assert;
use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException;
use D3\Webauthn\Application\Model\Webauthn; use D3\Webauthn\Application\Model\Webauthn;
@ -26,7 +29,6 @@ use Doctrine\DBAL\Driver\Exception as DoctrineDriverException;
use Doctrine\DBAL\Exception as DoctrineException; use Doctrine\DBAL\Exception as DoctrineException;
use OxidEsales\Eshop\Application\Controller\Admin\AdminController; use OxidEsales\Eshop\Application\Controller\Admin\AdminController;
use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Application\Controller\FrontendController;
use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\Request; use OxidEsales\Eshop\Core\Request;
use OxidEsales\Eshop\Core\Routing\ControllerClassNameResolver; use OxidEsales\Eshop\Core\Routing\ControllerClassNameResolver;
use OxidEsales\Eshop\Core\Session; use OxidEsales\Eshop\Core\Session;
@ -60,11 +62,11 @@ class d3webauthnadminlogin extends AdminController
public function render(): string public function render(): string
{ {
if (d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) if (d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->hasVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_AUTH) ->hasVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_AUTH)
) { ) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=admin_start'); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=admin_start');
} elseif (!d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) } elseif (!d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->hasVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) ->hasVariable(WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER)
) { ) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=login'); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=login');
} }
@ -107,11 +109,18 @@ class d3webauthnadminlogin extends AdminController
$this->addTplParam('isAdmin', isAdmin()); $this->addTplParam('isAdmin', isAdmin());
} catch (WebauthnException $e) { } catch (WebauthnException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->setVariable(WebauthnConf::GLOBAL_SWITCH, true); ->setVariable(WebauthnConf::GLOBAL_SWITCH, true);
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e); d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId' => $userId]); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId' => $userId]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString()); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=login'); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=login');
} catch (AssertionFailedException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->setVariable(WebauthnConf::GLOBAL_SWITCH, true);
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getMessage(), ['UserId' => $userId]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=login');
} }
} }
@ -122,10 +131,10 @@ class d3webauthnadminlogin extends AdminController
{ {
try { try {
$login = $this->getWebAuthnLogin(); $login = $this->getWebAuthnLogin();
return $login->adminLogin( $profile = d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('profile');
d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('profile') Assert::that($profile)->string();
); return $login->adminLogin($profile);
} catch (WebauthnGetException $e) { } catch (WebauthnGetException|AssertionFailedException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e); d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
return 'login'; return 'login';
} }
@ -165,16 +174,19 @@ class d3webauthnadminlogin extends AdminController
/** /**
* @return WebauthnLogin * @return WebauthnLogin
* @throws InvalidArgumentException
*/ */
protected function getWebAuthnLogin(): WebauthnLogin protected function getWebAuthnLogin(): WebauthnLogin
{ {
/** @var Request $request */ /** @var Request $request */
$request = d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class); $request = d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class);
return oxNew( $credential = $request->getRequestEscapedParameter('credential');
WebauthnLogin::class, $error = $request->getRequestEscapedParameter('error');
$request->getRequestEscapedParameter('credential'),
$request->getRequestEscapedParameter('error') Assert::that($credential)->string('credential value expected to be string');
); Assert::that($error)->string('error value expected to be string');
return oxNew(WebauthnLogin::class, $credential, $error);
} }
} }

View File

@ -15,7 +15,9 @@ declare(strict_types=1);
namespace D3\Webauthn\Application\Controller; namespace D3\Webauthn\Application\Controller;
use Assert\Assert;
use Assert\AssertionFailedException; use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Controller\Traits\accountTrait; use D3\Webauthn\Application\Controller\Traits\accountTrait;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredential; use D3\Webauthn\Application\Model\Credential\PublicKeyCredential;
@ -86,6 +88,10 @@ class d3_account_webauthn extends AccountController
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId: ' => $this->getUser()->getId()]); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getDetailedErrorMessage(), ['UserId: ' => $this->getUser()->getId()]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString()); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e); d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
} catch (AssertionFailedException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getMessage(), ['UserId: ' => $this->getUser()->getId()]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class)->addErrorToDisplay($e);
} }
} }
@ -103,6 +109,7 @@ class d3_account_webauthn extends AccountController
* @throws DoctrineException * @throws DoctrineException
* @throws ContainerExceptionInterface * @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface * @throws NotFoundExceptionInterface
* @throws InvalidArgumentException
* @return void * @return void
*/ */
public function setAuthnRegister(): void public function setAuthnRegister(): void
@ -137,11 +144,10 @@ class d3_account_webauthn extends AccountController
} }
$credential = d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('credential'); $credential = d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('credential');
if (strlen((string) $credential)) { Assert::that($credential)->minLength(1, 'Credential should not be empty.');
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($credential); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($credential);
$webauthn = d3GetOxidDIC()->get(Webauthn::class); $webauthn = d3GetOxidDIC()->get(Webauthn::class);
$webauthn->saveAuthn($credential, d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('keyname')); $webauthn->saveAuthn($credential, d3GetOxidDIC()->get('d3ox.webauthn.'.Request::class)->getRequestEscapedParameter('keyname'));
}
} catch (WebauthnException $e) { } catch (WebauthnException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error( d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error(
$e->getDetailedErrorMessage(), $e->getDetailedErrorMessage(),

View File

@ -15,6 +15,7 @@ declare(strict_types=1);
namespace D3\Webauthn\Application\Controller; namespace D3\Webauthn\Application\Controller;
use Assert\AssertionFailedException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Model\Webauthn; use D3\Webauthn\Application\Model\Webauthn;
use D3\Webauthn\Application\Model\WebauthnConf; use D3\Webauthn\Application\Model\WebauthnConf;
@ -62,9 +63,9 @@ class d3webauthnlogin extends FrontendController
public function render(): string public function render(): string
{ {
if (d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) if (d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->hasVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH) || ->hasVariable(WebauthnConf::WEBAUTHN_SESSION_AUTH) ||
!d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) !d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->hasVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER) ->hasVariable(WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER)
) { ) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=start'); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=start');
} }
@ -72,7 +73,7 @@ class d3webauthnlogin extends FrontendController
$this->generateCredentialRequest(); $this->generateCredentialRequest();
$this->addTplParam('navFormParams', d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) $this->addTplParam('navFormParams', d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->getVariable(WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS)); ->getVariable(WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS));
return $this->d3CallMockableFunction([FrontendController::class, 'render']); return $this->d3CallMockableFunction([FrontendController::class, 'render']);
} }
@ -103,6 +104,13 @@ class d3webauthnlogin extends FrontendController
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString()); d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
Registry::getUtilsView()->addErrorToDisplay($e); Registry::getUtilsView()->addErrorToDisplay($e);
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=start'); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=start');
} catch (AssertionFailedException $e) {
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
->setVariable(WebauthnConf::GLOBAL_SWITCH, true);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->error($e->getMessage(), ['UserId' => $userId]);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug($e->getTraceAsString());
Registry::getUtilsView()->addErrorToDisplay($e->getMessage());
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect('index.php?cl=start');
} }
} }

View File

@ -17,8 +17,9 @@ namespace D3\Webauthn\Application\Model\Credential;
use Assert\Assert; use Assert\Assert;
use Assert\AssertionFailedException; use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Setup\Actions; use D3\Webauthn\Migrations\Version20230209212939;
use DateTime; use DateTime;
use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException;
use Doctrine\DBAL\Exception as DoctrineException; use Doctrine\DBAL\Exception as DoctrineException;
@ -75,7 +76,7 @@ class PublicKeyCredential extends BaseModel
Assert::that($encodedCID) Assert::that($encodedCID)
->maxLength( ->maxLength(
Actions::FIELDLENGTH_CREDID, Version20230209212939::FIELDLENGTH_CREDID,
'the credentialId (%3$d) does not fit into the database field (%2$d)' 'the credentialId (%3$d) does not fit into the database field (%2$d)'
); );
@ -85,11 +86,16 @@ class PublicKeyCredential extends BaseModel
} }
/** /**
* @return null|string * @return string
* @throws InvalidArgumentException
*/ */
public function getCredentialId(): ?string public function getCredentialId(): ?string
{ {
return base64_decode($this->__get($this->_getFieldLongName('credentialid'))->rawValue) ?: null; $encodedCID = $this->__get($this->_getFieldLongName('credentialid'))->rawValue;
Assert::that($encodedCID)->base64('Credential ID "%s" is not a valid base64 string.');
return base64_decode($encodedCID);
} }
/** /**
@ -120,7 +126,7 @@ class PublicKeyCredential extends BaseModel
Assert::that($encodedCredential) Assert::that($encodedCredential)
->maxLength( ->maxLength(
Actions::FIELDLENGTH_CREDENTIAL, Version20230209212939::FIELDLENGTH_CREDENTIAL,
'the credential source (%3$d) does not fit into the database field (%2$d)', 'the credential source (%3$d) does not fit into the database field (%2$d)',
); );

View File

@ -15,7 +15,9 @@ declare(strict_types=1);
namespace D3\Webauthn\Application\Model; namespace D3\Webauthn\Application\Model;
use Assert\Assert;
use Assert\AssertionFailedException; use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredential; use D3\Webauthn\Application\Model\Credential\PublicKeyCredential;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList; use D3\Webauthn\Application\Model\Credential\PublicKeyCredentialList;
@ -24,7 +26,6 @@ use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException;
use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn; use D3\Webauthn\Modules\Application\Model\d3_User_Webauthn;
use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException;
use Doctrine\DBAL\Exception as DoctrineException; use Doctrine\DBAL\Exception as DoctrineException;
use Exception;
use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7\Factory\Psr17Factory;
use Nyholm\Psr7Server\ServerRequestCreator; use Nyholm\Psr7Server\ServerRequestCreator;
use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Model\User;
@ -76,6 +77,7 @@ class Webauthn
* @throws DoctrineDriverException * @throws DoctrineDriverException
* @throws DoctrineException * @throws DoctrineException
* @throws NotFoundExceptionInterface * @throws NotFoundExceptionInterface
* @throws InvalidArgumentException
*/ */
public function getCreationOptions(User $user): string public function getCreationOptions(User $user): string
{ {
@ -94,9 +96,7 @@ class Webauthn
$json = $this->jsonEncode($publicKeyCredentialCreationOptions); $json = $this->jsonEncode($publicKeyCredentialCreationOptions);
if ($json === false) { Assert::that($json)->isJsonString("can't encode request options");
throw oxNew(Exception::class, "can't encode creation options");
}
return $json; return $json;
} }
@ -134,6 +134,7 @@ class Webauthn
* @return string * @return string
* @throws DoctrineDriverException * @throws DoctrineDriverException
* @throws DoctrineException * @throws DoctrineException
* @throws InvalidArgumentException
*/ */
public function getRequestOptions(string $userId): string public function getRequestOptions(string $userId): string
{ {
@ -143,11 +144,16 @@ class Webauthn
d3GetOxidDIC()->set(UserEntity::class.'.args.user', $user); d3GetOxidDIC()->set(UserEntity::class.'.args.user', $user);
/** @var UserEntity $userEntity */ /** @var UserEntity $userEntity */
$userEntity = d3GetOxidDIC()->get(UserEntity::class); $userEntity = d3GetOxidDIC()->get(UserEntity::class);
$existingCredentials = $this->getExistingCredentials($userEntity);
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug(
'found user credentials: '.count($existingCredentials).' for ID '.$userId
);
// We generate the set of options. // We generate the set of options.
$publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions( $publicKeyCredentialRequestOptions = $this->getServer()->generatePublicKeyCredentialRequestOptions(
PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value PublicKeyCredentialRequestOptions::USER_VERIFICATION_REQUIREMENT_PREFERRED, // Default value
$this->getExistingCredentials($userEntity) $existingCredentials
); );
d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class) d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class)
@ -155,9 +161,11 @@ class Webauthn
$json = $this->jsonEncode($publicKeyCredentialRequestOptions); $json = $this->jsonEncode($publicKeyCredentialRequestOptions);
if ($json === false) { d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug(
throw oxNew(Exception::class, "can't encode request options"); 'request options: '.$json
} );
Assert::that($json)->isJsonString("can't encode request options");
return $json; return $json;
} }

View File

@ -17,6 +17,7 @@ namespace D3\Webauthn\Application\Model;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use OxidEsales\Eshop\Core\Language; use OxidEsales\Eshop\Core\Language;
use Psr\Log\LoggerInterface;
class WebauthnErrors class WebauthnErrors
{ {
@ -38,6 +39,10 @@ class WebauthnErrors
*/ */
public function translateError(string $msg, string $type = null): string public function translateError(string $msg, string $type = null): string
{ {
d3GetOxidDIC()->get('d3ox.webauthn.'.LoggerInterface::class)->debug(
'error occured: '.$msg
);
$lang = d3GetOxidDIC()->get('d3ox.webauthn.'.Language::class); $lang = d3GetOxidDIC()->get('d3ox.webauthn.'.Language::class);
$type = $type ? '_'.$type : null; $type = $type ? '_'.$type : null;

View File

@ -110,10 +110,11 @@ class WebauthnLogin
{ {
/** @var UtilsView $myUtilsView */ /** @var UtilsView $myUtilsView */
$myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class); $myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class);
/** @var d3_User_Webauthn $user */
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$userId = null;
try { try {
/** @var d3_User_Webauthn $user */
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$userId = $this->getUserId(); $userId = $this->getUserId();
$this->handleErrorMessage(); $this->handleErrorMessage();
@ -158,10 +159,11 @@ class WebauthnLogin
{ {
/** @var UtilsView $myUtilsView */ /** @var UtilsView $myUtilsView */
$myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class); $myUtilsView = d3GetOxidDIC()->get('d3ox.webauthn.'.UtilsView::class);
/** @var d3_User_Webauthn $user */
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$userId = null;
try { try {
/** @var d3_User_Webauthn $user */
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$userId = $this->getUserId(); $userId = $this->getUserId();
$this->handleErrorMessage(); $this->handleErrorMessage();
@ -195,8 +197,6 @@ class WebauthnLogin
$user->logout(); $user->logout();
$oStr = Str::getStr(); $oStr = Str::getStr();
d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getActiveView()
->addTplParam('user', $oStr->htmlspecialchars($userId));
d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getActiveView() d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getActiveView()
->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile)); ->addTplParam('profile', $oStr->htmlspecialchars($selectedProfile));

View File

@ -20,15 +20,15 @@ $aLang = [
'charset' => 'UTF-8', 'charset' => 'UTF-8',
'PAGE_TITLE_D3WEBAUTHNLOGIN' => 'Passwortloses Anmelden', 'PAGE_TITLE_D3WEBAUTHNLOGIN' => 'Passwortloses Anmelden',
'D3_WEBAUTHN_ACCOUNT' => 'Meine SchlĂĽssel', 'D3_WEBAUTHN_ACCOUNT' => 'Meine AnmeldeschlĂĽssel',
'PAGE_TITLE_D3_ACCOUNT_WEBAUTHN' => 'Meine SchlĂĽssel', 'PAGE_TITLE_D3_ACCOUNT_WEBAUTHN' => 'Meine AnmeldeschlĂĽssel',
'D3_WEBAUTHN_ACCOUNT_DESC' => 'Verwalten Sie hier Ihre AnmeldeschlĂĽssel.', 'D3_WEBAUTHN_ACCOUNT_DESC' => 'Verwalten Sie hier Ihre AnmeldeschlĂĽssel.',
'D3_WEBAUTHN_ACC_REGISTERNEW' => 'neue Registrierung erstellen', 'D3_WEBAUTHN_ACC_REGISTERNEW' => 'neue Registrierung erstellen',
'D3_WEBAUTHN_ACC_ADDKEY' => 'SicherheitsschlĂĽssel hinzufĂĽgen', 'D3_WEBAUTHN_ACC_ADDKEY' => 'AnmeldeschlĂĽssel hinzufĂĽgen',
'D3_WEBAUTHN_ACC_REGISTEREDKEYS' => 'registrierte SchlĂĽssel', 'D3_WEBAUTHN_ACC_REGISTEREDKEYS' => 'registrierte AnmeldeschlĂĽssel',
'WEBAUTHN_INPUT_HELP' => 'Bitte mit HardwareschlĂĽssel authentisieren.', 'WEBAUTHN_INPUT_HELP' => 'Bitte mit AnmeldeschlĂĽssel authentisieren.',
'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen', 'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen',
'D3_WEBAUTHN_BREADCRUMB' => 'Passwortloses Anmelden', 'D3_WEBAUTHN_BREADCRUMB' => 'Passwortloses Anmelden',
'D3_WEBAUTHN_CONFIRMATION' => 'Bestätigung erforderlich', 'D3_WEBAUTHN_CONFIRMATION' => 'Bestätigung erforderlich',
@ -37,15 +37,15 @@ $aLang = [
'D3_WEBAUTHN_DELETE' => 'Löschen', 'D3_WEBAUTHN_DELETE' => 'Löschen',
'D3_WEBAUTHN_DELETE_CONFIRM' => 'Soll der Schlüssel wirklich gelöscht werden?', 'D3_WEBAUTHN_DELETE_CONFIRM' => 'Soll der Schlüssel wirklich gelöscht werden?',
'D3_WEBAUTHN_KEYNAME' => 'Name des SchlĂĽssels', 'D3_WEBAUTHN_KEYNAME' => 'Name des SchlĂĽssels',
'D3_WEBAUTHN_NOKEYREGISTERED' => 'kein SchlĂĽssel registriert', 'D3_WEBAUTHN_NOKEYREGISTERED' => 'kein AnmeldeschlĂĽssel registriert',
'D3_WEBAUTHN_ACCOUNT_TYPE0' => 'nur Passwort', 'D3_WEBAUTHN_ACCOUNT_TYPE0' => 'nur Passwort',
'D3_WEBAUTHN_ACCOUNT_TYPE1' => 'nur Auth-SchlĂĽssel', 'D3_WEBAUTHN_ACCOUNT_TYPE1' => 'nur Auth-SchlĂĽssel',
'D3_WEBAUTHN_ACCOUNT_TYPE2' => 'nur Auth-SschlĂĽssel, Passwort als Alternative', 'D3_WEBAUTHN_ACCOUNT_TYPE2' => 'nur Auth-SschlĂĽssel, Passwort als Alternative',
'D3_WEBAUTHN_ACCOUNT_TYPE3' => 'Auth-SchlĂĽssel und Passwort in Kombination', 'D3_WEBAUTHN_ACCOUNT_TYPE3' => 'Auth-SchlĂĽssel und Passwort in Kombination',
'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'Die Verwendung von Sicherheitsschlüsseln ist nur bei lokalen oder gesicherten Verbindungen (https) möglich.', 'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'Die Verwendung von Anmeldeschlüsseln ist nur bei lokalen oder gesicherten Verbindungen (https) möglich.',
'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Die Anmeldung mit Sicherheitsschlüssel ist aus technischen Gründen derzeit leider nicht möglich. Bitte verwenden Sie statt dessen Ihr Passwort.', 'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Die Anmeldung mit Anmeldeschlüssel ist aus technischen Gründen derzeit leider nicht möglich. Bitte verwenden Sie statt dessen Ihr Passwort.',
'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Kann keine Anmeldedaten von nicht geladenem Kundenkonto beziehen.", 'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Kann keine Anmeldedaten von nicht geladenem Kundenkonto beziehen.",
'D3_WEBAUTHN_ERR_NOTCREDENTIALNOTSAVEABLE' => "Der SchlĂĽssel kann aus technischen GrĂĽnden nicht registriert werden. Bitte wenden Sie sich an den Shopbetreiber.", 'D3_WEBAUTHN_ERR_NOTCREDENTIALNOTSAVEABLE' => "Der AnmeldeschlĂĽssel kann aus technischen GrĂĽnden nicht registriert werden. Bitte wenden Sie sich an den Shopbetreiber.",
]; ];

View File

@ -20,15 +20,15 @@ $aLang = [
'charset' => 'UTF-8', 'charset' => 'UTF-8',
'PAGE_TITLE_D3WEBAUTHNLOGIN' => 'Passwordless login', 'PAGE_TITLE_D3WEBAUTHNLOGIN' => 'Passwordless login',
'D3_WEBAUTHN_ACCOUNT' => 'My keys', 'D3_WEBAUTHN_ACCOUNT' => 'My login keys',
'PAGE_TITLE_D3_ACCOUNT_WEBAUTHN' => 'My keys', 'PAGE_TITLE_D3_ACCOUNT_WEBAUTHN' => 'My login keys',
'D3_WEBAUTHN_ACCOUNT_DESC' => 'Manage your login keys here.', 'D3_WEBAUTHN_ACCOUNT_DESC' => 'Manage your login keys here.',
'D3_WEBAUTHN_ACC_REGISTERNEW' => 'create new registration', 'D3_WEBAUTHN_ACC_REGISTERNEW' => 'create new registration',
'D3_WEBAUTHN_ACC_ADDKEY' => 'add security key', 'D3_WEBAUTHN_ACC_ADDKEY' => 'add login key',
'D3_WEBAUTHN_ACC_REGISTEREDKEYS' => 'registered keys', 'D3_WEBAUTHN_ACC_REGISTEREDKEYS' => 'registered login keys',
'WEBAUTHN_INPUT_HELP' => 'Please authenticate with hardware key.', 'WEBAUTHN_INPUT_HELP' => 'Please authenticate with login key.',
'WEBAUTHN_CANCEL_LOGIN' => 'Cancel login', 'WEBAUTHN_CANCEL_LOGIN' => 'Cancel login',
'D3_WEBAUTHN_BREADCRUMB' => 'Passwordless login', 'D3_WEBAUTHN_BREADCRUMB' => 'Passwordless login',
'D3_WEBAUTHN_CONFIRMATION' => 'Confirmation required', 'D3_WEBAUTHN_CONFIRMATION' => 'Confirmation required',
@ -37,15 +37,15 @@ $aLang = [
'D3_WEBAUTHN_DELETE' => 'delete', 'D3_WEBAUTHN_DELETE' => 'delete',
'D3_WEBAUTHN_DELETE_CONFIRM' => 'Do you really want to delete the key?', 'D3_WEBAUTHN_DELETE_CONFIRM' => 'Do you really want to delete the key?',
'D3_WEBAUTHN_KEYNAME' => 'name of the key', 'D3_WEBAUTHN_KEYNAME' => 'name of the key',
'D3_WEBAUTHN_NOKEYREGISTERED' => 'no key registered', 'D3_WEBAUTHN_NOKEYREGISTERED' => 'no login key registered',
'D3_WEBAUTHN_ACCOUNT_TYPE0' => 'password only', 'D3_WEBAUTHN_ACCOUNT_TYPE0' => 'password only',
'D3_WEBAUTHN_ACCOUNT_TYPE1' => 'auth keys only', 'D3_WEBAUTHN_ACCOUNT_TYPE1' => 'auth keys only',
'D3_WEBAUTHN_ACCOUNT_TYPE2' => 'auth keys only, password as an alternative', 'D3_WEBAUTHN_ACCOUNT_TYPE2' => 'auth keys only, password as an alternative',
'D3_WEBAUTHN_ACCOUNT_TYPE3' => 'auth key and password combined', 'D3_WEBAUTHN_ACCOUNT_TYPE3' => 'auth key and password combined',
'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'The use of security keys is only possible with local or secured connections (https).', 'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'The use of login keys is only possible with local or secured connections (https).',
'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Unfortunately, logging in with a security key is currently not possible for technical reasons. Please use your password instead.', 'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Unfortunately, logging in with a login key is currently not possible for technical reasons. Please use your password instead.',
'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Cannot obtain login data from unloaded customer account.", 'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Cannot obtain login data from unloaded customer account.",
'D3_WEBAUTHN_ERR_NOTCREDENTIALNOTSAVEABLE' => "The key cannot be registered for technical reasons. Please contact the shop operator.", 'D3_WEBAUTHN_ERR_NOTCREDENTIALNOTSAVEABLE' => "The login key cannot be registered for technical reasons. Please contact the shop operator.",
]; ];

View File

@ -18,35 +18,35 @@ $sLangName = "Deutsch";
$aLang = [ $aLang = [
'charset' => 'UTF-8', 'charset' => 'UTF-8',
'D3_WEBAUTHN_ERROR_UNVALID' => 'Der verwendete SchlĂĽssel ist ungĂĽltig oder kann nicht geprĂĽft werden.', 'D3_WEBAUTHN_ERROR_UNVALID' => 'Der verwendete AnmeldeschlĂĽssel ist ungĂĽltig oder kann nicht geprĂĽft werden.',
'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'Keine prĂĽfbaren Anfrageoptionen gespeichert. Bitte fĂĽhren Sie die Anmeldung noch einmal durch bzw. wenden sich an den Betreiber.', 'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'Keine prĂĽfbaren Anfrageoptionen gespeichert. Bitte fĂĽhren Sie die Anmeldung noch einmal durch bzw. wenden sich an den Betreiber.',
'WEBAUTHN_INPUT_HELP' => 'Bitte mit HardwareschlĂĽssel authentisieren.', 'WEBAUTHN_INPUT_HELP' => 'Bitte mit AnmeldeschlĂĽssel authentisieren.',
'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen', 'WEBAUTHN_CANCEL_LOGIN' => 'Anmeldung abbrechen',
'D3WEBAUTHN_CONF_BROWSER_REQUEST' => 'Bitte die Anfrage des Browsers bestätigen:', 'D3WEBAUTHN_CONF_BROWSER_REQUEST' => 'Bitte die Anfrage des Browsers bestätigen:',
'D3WEBAUTHN_CANCEL' => 'Abbrechen', 'D3WEBAUTHN_CANCEL' => 'Abbrechen',
'D3WEBAUTHN_DELETE' => 'Löschen', 'D3WEBAUTHN_DELETE' => 'Löschen',
'D3WEBAUTHN_DELETE_CONFIRM' => 'Soll der Schlüssel wirklich gelöscht werden?', 'D3WEBAUTHN_DELETE_CONFIRM' => 'Soll der Anmeldeschlüssel wirklich gelöscht werden?',
'D3WEBAUTHN_CANCELNOKEYREGISTERED' => 'kein SchlĂĽssel registriert', 'D3WEBAUTHN_CANCELNOKEYREGISTERED' => 'kein AnmeldeschlĂĽssel registriert',
'd3mxuser_webauthn' => 'HardwareschlĂĽssel', 'd3mxuser_webauthn' => 'AnmeldeschlĂĽssel',
'D3_WEBAUTHN_REGISTERNEW' => 'neue Registrierung erstellen', 'D3_WEBAUTHN_REGISTERNEW' => 'neue Registrierung erstellen',
'D3_WEBAUTHN_ADDKEY' => 'SicherheitsschlĂĽssel hinzufĂĽgen', 'D3_WEBAUTHN_ADDKEY' => 'AnmeldeschlĂĽssel hinzufĂĽgen',
'D3_WEBAUTHN_KEYNAME' => 'Name des SchlĂĽssels', 'D3_WEBAUTHN_KEYNAME' => 'Name des SchlĂĽssels',
'D3_WEBAUTHN_REGISTEREDKEYS' => 'registrierte SchlĂĽssel', 'D3_WEBAUTHN_REGISTEREDKEYS' => 'registrierte AnmeldeschlĂĽssel',
'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'Die Verwendung von Sicherheitsschlüsseln ist nur bei lokalen oder gesicherten Verbindungen (https) möglich.', 'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'Die Verwendung von Anmeldeschlüsseln ist nur bei lokalen oder gesicherten Verbindungen (https) möglich.',
'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_CREATE => 'Der Schlüssel vom Token kann nicht oder nicht mehr verwendet werden. Möglicherweise wurde dieser in Ihrem Konto schon einmal gespeichert.', 'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_CREATE => 'Der AnmeldeSchlüssel kann nicht oder nicht mehr verwendet werden. Möglicherweise wurde dieser in Ihrem Konto schon einmal gespeichert.',
'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_GET => 'Der SchlĂĽssel kann nicht validiert werden.', 'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_GET => 'Der AnmeldeschlĂĽssel kann nicht validiert werden.',
'D3_WEBAUTHN_ERR_NOTALLOWED' => 'Die Anfrage wurde vom Browser oder der Plattform nicht zugelassen. Möglicherweise fehlen Berechtigungen oder die Zeit ist abgelaufen.', 'D3_WEBAUTHN_ERR_NOTALLOWED' => 'Die Anfrage wurde vom Browser oder der Plattform nicht zugelassen. Möglicherweise fehlen Berechtigungen oder die Zeit ist abgelaufen.',
'D3_WEBAUTHN_ERR_ABORT' => 'Die Aktion wurde vom Browser oder der Plattform abgebrochen.', 'D3_WEBAUTHN_ERR_ABORT' => 'Die Aktion wurde vom Browser oder der Plattform abgebrochen.',
'D3_WEBAUTHN_ERR_CONSTRAINT' => 'Die Aktion konnte vom authentisierenden Gerät nicht durchgeführt werden.', 'D3_WEBAUTHN_ERR_CONSTRAINT' => 'Die Aktion konnte vom authentisierenden Gerät nicht durchgeführt werden.',
'D3_WEBAUTHN_ERR_NOTSUPPORTED' => 'Die Aktion wird nicht unterstĂĽtzt.', 'D3_WEBAUTHN_ERR_NOTSUPPORTED' => 'Die Aktion wird nicht unterstĂĽtzt.',
'D3_WEBAUTHN_ERR_UNKNOWN' => 'Die Aktion wurde wegen eines unbekannten Fehlers abgebrochen.', 'D3_WEBAUTHN_ERR_UNKNOWN' => 'Die Aktion wurde wegen eines unbekannten Fehlers abgebrochen.',
'D3_WEBAUTHN_ERR_NOPUBKEYSUPPORT' => 'Ihr Browser unterstĂĽtzt die Verwendung von HardwareschlĂĽsseln leider nicht.', 'D3_WEBAUTHN_ERR_NOPUBKEYSUPPORT' => 'Ihr Browser unterstĂĽtzt die Verwendung von AnmeldeschlĂĽsseln leider nicht.',
'D3_WEBAUTHN_ERR_TECHNICALERROR' => 'Beim PrĂĽfen der Zugangsdaten ist ein technischer Fehler aufgetreten.', 'D3_WEBAUTHN_ERR_TECHNICALERROR' => 'Beim PrĂĽfen der Zugangsdaten ist ein technischer Fehler aufgetreten.',
'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Kann keine Anmeldedaten von nicht geladenem Kundenkonto beziehen.", 'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Kann keine Anmeldedaten von nicht geladenem Kundenkonto beziehen.",
'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Die Anmeldung mit Sicherheitsschlüssel ist aus technischen Gründen derzeit leider nicht möglich. Bitte verwenden Sie statt dessen Ihr Passwort.', 'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Die Anmeldung mit Anmeldeschlüssel ist aus technischen Gründen derzeit leider nicht möglich. Bitte verwenden Sie statt dessen Ihr Passwort.',
]; ];

View File

@ -18,35 +18,35 @@ $sLangName = "English";
$aLang = [ $aLang = [
'charset' => 'UTF-8', 'charset' => 'UTF-8',
'D3_WEBAUTHN_ERROR_UNVALID' => 'The key used is invalid or cannot be checked.', 'D3_WEBAUTHN_ERROR_UNVALID' => 'The used login key is invalid or cannot be checked.',
'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'No verifiable request options saved. Please perform the registration again or contact the operator.', 'D3_WEBAUTHN_ERROR_MISSINGPKC' => 'No verifiable request options saved. Please perform the registration again or contact the operator.',
'WEBAUTHN_INPUT_HELP' => 'Please authenticate with hardware key.', 'WEBAUTHN_INPUT_HELP' => 'Please authenticate with login key.',
'WEBAUTHN_CANCEL_LOGIN' => 'Cancel login', 'WEBAUTHN_CANCEL_LOGIN' => 'Cancel login',
'D3WEBAUTHN_CONF_BROWSER_REQUEST' => 'Please confirm the browser request:', 'D3WEBAUTHN_CONF_BROWSER_REQUEST' => 'Please confirm the browser request:',
'D3WEBAUTHN_CANCEL' => 'Cancel', 'D3WEBAUTHN_CANCEL' => 'Cancel',
'D3WEBAUTHN_DELETE' => 'Delete', 'D3WEBAUTHN_DELETE' => 'Delete',
'D3WEBAUTHN_DELETE_CONFIRM' => 'Do you really want to delete the key?', 'D3WEBAUTHN_DELETE_CONFIRM' => 'Do you really want to delete the login key?',
'D3WEBAUTHN_CANCELNOKEYREGISTERED' => 'No key registered', 'D3WEBAUTHN_CANCELNOKEYREGISTERED' => 'No login key registered',
'd3mxuser_webauthn' => 'hardware key', 'd3mxuser_webauthn' => 'login keys',
'D3_WEBAUTHN_REGISTERNEW' => 'create new registration', 'D3_WEBAUTHN_REGISTERNEW' => 'create new registration',
'D3_WEBAUTHN_ADDKEY' => 'add security key', 'D3_WEBAUTHN_ADDKEY' => 'add login key',
'D3_WEBAUTHN_KEYNAME' => 'Key name', 'D3_WEBAUTHN_KEYNAME' => 'Key name',
'D3_WEBAUTHN_REGISTEREDKEYS' => 'registered keys', 'D3_WEBAUTHN_REGISTEREDKEYS' => 'registered login keys',
'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'The use of security keys is only possible with local or secure connections (https).', 'D3_WEBAUTHN_ERR_UNSECURECONNECTION' => 'The use of login keys is only possible with local or secure connections (https).',
'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_CREATE => 'The key from the token cannot be used or can no longer be used. It may have been stored in your account before.', 'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_CREATE => 'The login key from the token cannot be used or can no longer be used. It may have been stored in your account before.',
'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_GET => 'The key cannot be validated.', 'D3_WEBAUTHN_ERR_INVALIDSTATE_'.WebauthnConf::TYPE_GET => 'The login key cannot be validated.',
'D3_WEBAUTHN_ERR_NOTALLOWED' => 'The request was not allowed by the browser or the platform. Possibly permissions are missing or the time has expired.', 'D3_WEBAUTHN_ERR_NOTALLOWED' => 'The request was not allowed by the browser or the platform. Possibly permissions are missing or the time has expired.',
'D3_WEBAUTHN_ERR_ABORT' => 'The action was aborted by the browser or the platform.', 'D3_WEBAUTHN_ERR_ABORT' => 'The action was aborted by the browser or the platform.',
'D3_WEBAUTHN_ERR_CONSTRAINT' => 'The action could not be performed by the authenticating device.', 'D3_WEBAUTHN_ERR_CONSTRAINT' => 'The action could not be performed by the authenticating device.',
'D3_WEBAUTHN_ERR_NOTSUPPORTED' => 'The action is not supported.', 'D3_WEBAUTHN_ERR_NOTSUPPORTED' => 'The action is not supported.',
'D3_WEBAUTHN_ERR_UNKNOWN' => 'The action was cancelled due to an unknown error.', 'D3_WEBAUTHN_ERR_UNKNOWN' => 'The action was cancelled due to an unknown error.',
'D3_WEBAUTHN_ERR_NOPUBKEYSUPPORT' => 'Unfortunately, your browser does not support the use of hardware keys.', 'D3_WEBAUTHN_ERR_NOPUBKEYSUPPORT' => 'Unfortunately, your browser does not support the use of login keys.',
'D3_WEBAUTHN_ERR_TECHNICALERROR' => 'A technical error occurred while checking the access data.', 'D3_WEBAUTHN_ERR_TECHNICALERROR' => 'A technical error occurred while checking the access data.',
'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Can't create webauthn user entity from not loaded user", 'D3_WEBAUTHN_ERR_NOTLOADEDUSER' => "Can't create webauthn user entity from not loaded user",
'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Unfortunately, logging in with a security key is currently not possible for technical reasons. Please use your password instead.', 'D3_WEBAUTHN_ERR_LOGINPROHIBITED' => 'Unfortunately, logging in with a login key is currently not possible for technical reasons. Please use your password instead.',
]; ];

View File

@ -136,3 +136,10 @@ services:
arguments: arguments:
- 2 - 2
shared: true shared: true
d3ox.webauthn.OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder:
class: 'OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder'
factory: 'oxNew'
arguments:
- 'OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder'
shared: false

View File

@ -17,6 +17,7 @@ namespace D3\Webauthn\Modules\Application\Component;
use Assert\Assert; use Assert\Assert;
use Assert\AssertionFailedException; use Assert\AssertionFailedException;
use Assert\InvalidArgumentException;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException; use D3\Webauthn\Application\Model\Exceptions\WebauthnGetException;
use D3\Webauthn\Application\Model\Exceptions\WebauthnLoginErrorException; use D3\Webauthn\Application\Model\Exceptions\WebauthnLoginErrorException;
@ -65,29 +66,27 @@ class d3_webauthn_UserComponent extends d3_webauthn_UserComponent_parent
$user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class); $user = d3GetOxidDIC()->get('d3ox.webauthn.'.User::class);
$userId = $user->d3GetLoginUserId($lgn_user); $userId = $user->d3GetLoginUserId($lgn_user);
if ($this->d3CanUseWebauthn($lgn_user, $userId)) { if ($this->d3CanUseWebauthn($lgn_user, $userId) && $this->d3HasWebauthnButNotLoggedin($userId)) {
if ($this->d3HasWebauthnButNotLoggedin($userId)) { $session = d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class);
$session = d3GetOxidDIC()->get('d3ox.webauthn.'.Session::class); $session->setVariable(
$session->setVariable( WebauthnConf::WEBAUTHN_SESSION_CURRENTCLASS,
WebauthnConf::WEBAUTHN_SESSION_CURRENTCLASS, $this->getClassKey() != 'd3webauthnlogin' ? $this->getClassKey() : 'start'
$this->getClassKey() != 'd3webauthnlogin' ? $this->getClassKey() : 'start' );
); $session->setVariable(
$session->setVariable( WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER,
WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER, $userId
$userId );
); $session->setVariable(
$session->setVariable( WebauthnConf::WEBAUTHN_SESSION_NAVPARAMS,
WebauthnConf::WEBAUTHN_SESSION_NAVPARAMS, $this->getParent()->getNavigationParams()
$this->getParent()->getNavigationParams() );
); $session->setVariable(
$session->setVariable( WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS,
WebauthnConf::WEBAUTHN_SESSION_NAVFORMPARAMS, $this->getParent()->getViewConfig()->getNavFormParams()
$this->getParent()->getViewConfig()->getNavFormParams() );
);
$sUrl = d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getShopHomeUrl() . 'cl=d3webauthnlogin'; $sUrl = d3GetOxidDIC()->get('d3ox.webauthn.'.Config::class)->getShopHomeUrl() . 'cl=d3webauthnlogin';
d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect($sUrl); d3GetOxidDIC()->get('d3ox.webauthn.'.Utils::class)->redirect($sUrl);
}
} }
} }
@ -163,6 +162,7 @@ class d3_webauthn_UserComponent extends d3_webauthn_UserComponent_parent
/** /**
* @return WebauthnLogin * @return WebauthnLogin
* @throws InvalidArgumentException
*/ */
protected function d3GetWebauthnLogin(): WebauthnLogin protected function d3GetWebauthnLogin(): WebauthnLogin
{ {
@ -172,8 +172,7 @@ class d3_webauthn_UserComponent extends d3_webauthn_UserComponent_parent
$credential = $request->getRequestEscapedParameter('credential'); $credential = $request->getRequestEscapedParameter('credential');
$error = $request->getRequestEscapedParameter('error'); $error = $request->getRequestEscapedParameter('error');
Assert::that($credential)->string('credential value expected to be string') Assert::that($credential)->string('credential value expected to be string');
->notEmpty('credential value expected contained content');
Assert::that($error)->string('error value expected to be string'); Assert::that($error)->string('error value expected to be string');
return oxNew(WebauthnLogin::class, $credential, $error); return oxNew(WebauthnLogin::class, $credential, $error);

View File

@ -18,12 +18,10 @@ namespace D3\Webauthn\Setup;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use Doctrine\DBAL\Driver\Exception as DoctrineDriverException; use Doctrine\DBAL\Driver\Exception as DoctrineDriverException;
use Exception; use Exception;
use OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder;
use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Application\Controller\FrontendController;
use OxidEsales\Eshop\Core\Config; use OxidEsales\Eshop\Core\Config;
use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface;
use OxidEsales\Eshop\Core\DbMetaDataHandler; use OxidEsales\Eshop\Core\DbMetaDataHandler;
use OxidEsales\Eshop\Core\Exception\DatabaseConnectionException;
use OxidEsales\Eshop\Core\Exception\DatabaseErrorException;
use OxidEsales\Eshop\Core\SeoEncoder; use OxidEsales\Eshop\Core\SeoEncoder;
use OxidEsales\Eshop\Core\Utils; use OxidEsales\Eshop\Core\Utils;
use OxidEsales\Eshop\Core\UtilsView; use OxidEsales\Eshop\Core\UtilsView;
@ -39,97 +37,25 @@ use Psr\Log\LoggerInterface;
class Actions class Actions
{ {
use IsMockable; use IsMockable;
public const FIELDLENGTH_CREDID = 512;
public const FIELDLENGTH_CREDENTIAL = 2000;
public $seo_de = 'sicherheitsschluessel'; public $seo_de = 'anmeldeschluessel';
public $seo_en = 'en/key-authentication'; public $seo_en = 'en/login-keys';
public $stdClassName = 'd3_account_webauthn'; public $stdClassName = 'd3_account_webauthn';
/** /**
* SQL statement, that will be executed only at the first time of module installation. * @throws Exception
*
* @var string
*/ */
protected $createCredentialSql = public function runModuleMigrations()
"CREATE TABLE `d3wa_usercredentials` (
`OXID` char(32) NOT NULL,
`OXUSERID` char(32) NOT NULL,
`OXSHOPID` int(11) NOT NULL,
`NAME` varchar(100) NOT NULL,
`CREDENTIALID` varchar(".self::FIELDLENGTH_CREDID.") NOT NULL,
`CREDENTIAL` varchar(".self::FIELDLENGTH_CREDENTIAL.") NOT NULL,
`OXTIMESTAMP` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`OXID`),
KEY `CREDENTIALID_IDX` (`CREDENTIALID`),
KEY `SHOPUSER_IDX` (`OXUSERID`,`OXSHOPID`) USING BTREE
) ENGINE=InnoDB COMMENT='WebAuthn Credentials';";
/**
* Execute the sql at the first time of the module installation.
* @return void
* @throws DatabaseConnectionException
* @throws DatabaseErrorException
*/
public function setupModule()
{ {
if (!$this->tableExists('d3wa_usercredentials')) { /** @var MigrationsBuilder $migrationsBuilder */
$this->executeSQL($this->createCredentialSql); $migrationsBuilder = d3GetOxidDIC()->get('d3ox.webauthn.'.MigrationsBuilder::class);
} $migrations = $migrationsBuilder->build();
} $migrations->execute('migrations:migrate', 'd3webauthn');
/**
* Check if table exists
*
* @param string $sTableName table name
*
* @return bool
*/
public function tableExists(string $sTableName): bool
{
$oDbMetaDataHandler = d3GetOxidDIC()->get('d3ox.webauthn.'.DbMetaDataHandler::class);
return $oDbMetaDataHandler->tableExists($sTableName);
}
/**
* @return DatabaseInterface|null
* @throws DatabaseConnectionException
*/
protected function d3GetDb(): ?DatabaseInterface
{
/** @var DatabaseInterface $db */
$db = d3GetOxidDIC()->get('d3ox.webauthn.'.DatabaseInterface::class.'.assoc');
return $db;
}
/**
* Executes given sql statement.
*
* @param string $sSQL Sql to execute.
* @throws DatabaseConnectionException
* @throws DatabaseErrorException
*/
public function executeSQL(string $sSQL)
{
$this->d3GetDb()->execute($sSQL);
}
/**
* Check if field exists in table
*
* @param string $sFieldName field name
* @param string $sTableName table name
*
* @return bool
*/
public function fieldExists(string $sFieldName, string $sTableName): bool
{
$oDbMetaDataHandler = d3GetOxidDIC()->get('d3ox.webauthn.'.DbMetaDataHandler::class);
return $oDbMetaDataHandler->fieldExists($sFieldName, $sTableName);
} }
/** /**
* Regenerate views for changed tables * Regenerate views for changed tables
* @throws Exception
*/ */
public function regenerateViews() public function regenerateViews()
{ {
@ -139,6 +65,7 @@ class Actions
/** /**
* clear cache * clear cache
* @throws Exception
*/ */
public function clearCache() public function clearCache()
{ {
@ -204,6 +131,7 @@ class Actions
/** /**
* @return void * @return void
* @throws Exception
*/ */
public function seoUrl() public function seoUrl()
{ {
@ -220,9 +148,11 @@ class Actions
/** /**
* @return bool * @return bool
* @throws Exception
*/ */
public function hasSeoUrl(): bool public function hasSeoUrl(): bool
{ {
/** @var SeoEncoder $seoEncoder */
$seoEncoder = d3GetOxidDIC()->get('d3ox.webauthn.'.SeoEncoder::class); $seoEncoder = d3GetOxidDIC()->get('d3ox.webauthn.'.SeoEncoder::class);
$seoUrl = $seoEncoder->getStaticUrl( $seoUrl = $seoEncoder->getStaticUrl(
d3GetOxidDIC()->get('d3ox.webauthn.'.FrontendController::class)->getViewConfig()->getSelfLink() . d3GetOxidDIC()->get('d3ox.webauthn.'.FrontendController::class)->getViewConfig()->getSelfLink() .
@ -234,6 +164,7 @@ class Actions
/** /**
* @return void * @return void
* @throws Exception
*/ */
public function createSeoUrl() public function createSeoUrl()
{ {

View File

@ -34,7 +34,7 @@ class Events
} }
$actions = oxNew(Actions::class); $actions = oxNew(Actions::class);
$actions->setupModule(); $actions->runModuleMigrations();
$actions->regenerateViews(); $actions->regenerateViews();
$actions->clearCache(); $actions->clearCache();
$actions->seoUrl(); $actions->seoUrl();

BIN
src/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -66,14 +66,15 @@ $logo = '<img src="https://logos.oxidmodule.com/d3logo.svg" alt="(D3)" style="he
$aModule = [ $aModule = [
'id' => $sModuleId, 'id' => $sModuleId,
'title' => [ 'title' => [
'de' => $logo.' zweiter Faktor - Passwortlose Anmeldung', 'de' => $logo.' zweiter Faktor - Passwortlose Anmeldung mit passkeys',
'en' => $logo.' second factor - passwordless login', 'en' => $logo.' second factor - passwordless login with passkeys',
], ],
'description' => [ 'description' => [
'de' => 'Passwortlose Anmeldung f&uuml;r OXID eSales Shop (WebAuthn / FIDO2 basiert)', 'de' => 'Passwortlose Anmeldung f&uuml;r OXID eSales Shop (mit WebAuthn / FIDO2 basierten passkeys)',
'en' => 'Passwordless login for OXID eSales shop (WebAuthn / FIDO2 based)', 'en' => 'Passwordless login for OXID eSales shop (with WebAuthn / FIDO2 based passkeys)',
], ],
'version' => '1.0.0.0', 'version' => '1.0.0.0',
'thumbnail' => 'logo.png',
'author' => 'D&sup3; Data Development (Inh.: Thomas Dartsch)', 'author' => 'D&sup3; Data Development (Inh.: Thomas Dartsch)',
'email' => 'support@shopmodule.com', 'email' => 'support@shopmodule.com',
'url' => 'https://www.oxidmodule.com/', 'url' => 'https://www.oxidmodule.com/',

View File

@ -0,0 +1,112 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @link https://www.oxidmodule.com
*/
declare(strict_types=1);
namespace D3\Webauthn\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Types\DateTimeType;
use Doctrine\DBAL\Types\IntegerType;
use Doctrine\DBAL\Types\StringType;
use Doctrine\Migrations\AbstractMigration;
final class Version20230209212939 extends AbstractMigration
{
public const FIELDLENGTH_CREDID = 512;
public const FIELDLENGTH_CREDENTIAL = 2000;
public function getDescription() : string
{
return 'create credential database table';
}
public function up(Schema $schema) : void
{
$this->connection->getDatabasePlatform()->registerDoctrineTypeMapping('enum', 'string');
$table = !$schema->hasTable('d3wa_usercredentials') ?
$schema->createTable('d3wa_usercredentials') :
$schema->getTable('d3wa_usercredentials');
if (!$table->hasColumn('OXID')) {
$table->addColumn('OXID', (new StringType())->getName())
->setLength(32)
->setFixed(true)
->setNotnull(true);
}
if (!$table->hasColumn('OXUSERID')) {
$table->addColumn('OXUSERID', (new StringType())->getName())
->setLength(32)
->setFixed(true)
->setNotnull(true);
}
if (!$table->hasColumn('OXSHOPID')) {
$table->addColumn('OXSHOPID', (new IntegerType())->getName())
->setLength(11)
->setNotnull(true);
}
if (!$table->hasColumn('NAME')) {
$table->addColumn('NAME', (new StringType())->getName())
->setLength(100)
->setFixed(false)
->setNotnull(true);
}
if (!$table->hasColumn('CREDENTIALID')) {
$table->addColumn('CREDENTIALID', (new StringType())->getName())
->setLength(self::FIELDLENGTH_CREDID)
->setFixed(false)
->setNotnull(true);
}
if (!$table->hasColumn('CREDENTIAL')) {
$table->addColumn('CREDENTIAL', (new StringType())->getName())
->setLength(self::FIELDLENGTH_CREDENTIAL)
->setFixed(false)
->setNotnull(true);
}
if (!$table->hasColumn('OXTIMESTAMP')) {
$table->addColumn('OXTIMESTAMP', (new DateTimeType())->getName())
->setType(new DateTimeType())
->setNotnull(true)
// can't set default value via default method
->setColumnDefinition('timestamp DEFAULT CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP');
}
if (!$table->hasPrimaryKey()) {
$table->setPrimaryKey(['OXID']);
}
if (!$table->hasIndex('SHOPUSER_IDX')) {
$table->addIndex(['OXUSERID', 'OXSHOPID'], 'SHOPUSER_IDX');
}
if (!$table->hasIndex('CREDENTIALID_IDX')) {
$table->addIndex(['CREDENTIALID'], 'CREDENTIALID_IDX');
}
$table->setComment('WebAuthn Credentials');
}
public function down(Schema $schema) : void
{
if ($schema->hasTable('d3wa_usercredentials')) {
$schema->dropTable('d3wa_usercredentials');
}
}
}

4
src/migration/migrations.yml Executable file
View File

@ -0,0 +1,4 @@
name: D3 Twofactor Passwordless
migrations_namespace: D3\Webauthn\Migrations
table_name: d3wa_migrations
migrations_directory: data

View File

@ -179,7 +179,7 @@ const requestCredentials = (publicKey) => {
response: { response: {
authenticatorData: base64ArrayBuffer(authenticateInfo.response.authenticatorData), authenticatorData: base64ArrayBuffer(authenticateInfo.response.authenticatorData),
signature: base64ArrayBuffer(authenticateInfo.response.signature), signature: base64ArrayBuffer(authenticateInfo.response.signature),
userHandle: authenticateInfo.response.userHandle, userHandle: base64ArrayBuffer(authenticateInfo.response.userHandle),
clientDataJSON: base64ArrayBuffer(authenticateInfo.response.clientDataJSON) clientDataJSON: base64ArrayBuffer(authenticateInfo.response.clientDataJSON)
}, },
type: authenticateInfo.type type: authenticateInfo.type

View File

@ -0,0 +1,14 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @link https://www.oxidmodule.com
*/
const D3WEBAUTHN_REQUIRE_MODCFG = false;

View File

@ -17,15 +17,15 @@
namespace D3\Webauthn\tests\integration; namespace D3\Webauthn\tests\integration;
use D3\ModCfg\Application\Model\DependencyInjectionContainer\d3DicHandler; use D3\DIContainerHandler\d3DicHandler;
use D3\ModCfg\Tests\unit\d3ModCfgUnitTestCase;
use Exception; use Exception;
use OxidEsales\Eshop\Application\Model\Article; use OxidEsales\Eshop\Application\Model\Article;
use OxidEsales\Eshop\Application\Model\Rights; use OxidEsales\Eshop\Application\Model\Rights;
use OxidEsales\Eshop\Application\Model\User; use OxidEsales\Eshop\Application\Model\User;
use OxidEsales\Eshop\Core\Model\BaseModel; use OxidEsales\Eshop\Core\Model\BaseModel;
use OxidEsales\TestingLibrary\UnitTestCase;
abstract class integrationTestCase extends d3ModCfgUnitTestCase abstract class integrationTestCase extends UnitTestCase
{ {
/** /**
* Set up fixture. * Set up fixture.

View File

@ -76,7 +76,7 @@ class passwordFrontendAuthTest extends integrationTestCase
* @test * @test
* @dataProvider loginDataProvider * @dataProvider loginDataProvider
*/ */
public function testCheckLoginReturn($username, $password, $expected) public function testCheckLoginReturn($username, $password, $expected, $redirect = null)
{ {
$_POST['lgn_usr'] = $username; $_POST['lgn_usr'] = $username;
$_POST['lgn_pwd'] = $password; $_POST['lgn_pwd'] = $password;

View File

@ -0,0 +1,167 @@
<?php
/**
* This Software is the property of Data Development and is protected
* by copyright law - it is NOT Freeware.
* Any unauthorized use of this software without a valid license
* is a violation of the license agreement and will be prosecuted by
* civil and criminal law.
* http://www.shopmodule.com
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <support@shopmodule.com>
* @link http://www.oxidmodule.com
*/
namespace D3\Webauthn\tests\integration;
use D3\DIContainerHandler\d3DicHandler;
use D3\Webauthn\Application\Model\Credential\PublicKeyCredential;
use OxidEsales\Eshop\Core\Utils;
class webauthnFrontendAuthTest extends passwordFrontendAuthTest
{
protected $userList = [
1 => 'userId1',
2 => 'userId2',
3 => 'userId3',
4 => 'userId4',
5 => 'userId5',
];
protected $credentialList = [
1 => 'credId1',
2 => 'credId2',
3 => 'credId3',
4 => 'credId4',
5 => 'credId5',
];
public function createTestData()
{
parent::createTestData();
$this->createUser(
$this->userList[5],
[
'oxactive' => 1,
'oxrights' => 'malladmin',
'oxshopid' => 1,
'oxusername' => 'wawrongshopid@user.localhost',
'oxpassword' => '$2y$10$QErMJNHQCoN03tfCUQDRfOvbwvqfzwWw1iI/7bC49fKQrPKoDdnaK', // 123456
'oxstreet' => __CLASS__,
],
true
);
$this->createObject(
PublicKeyCredential::class,
$this->credentialList[1],
[
'oxuserid' => $this->userList[1],
'oxshopid' => 1,
'name' => __CLASS__,
'credentialid' => 'ITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAAA==',
'credential'=> 'TzozNDoiV2ViYXV0aG5cUHVibGljS2V5Q3JlZGVudGlhbFNvdXJjZSI6MTA6e3M6MjQ6IgAqAHB1YmxpY0tleUNyZWRlbnRpYWxJZCI7czo3MDoiITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAACI7czo3OiIAKgB0eXBlIjtzOjEwOiJwdWJsaWMta2V5IjtzOjEzOiIAKgB0cmFuc3BvcnRzIjthOjA6e31zOjE4OiIAKgBhdHRlc3RhdGlvblR5cGUiO3M6NDoibm9uZSI7czoxMjoiACoAdHJ1c3RQYXRoIjtPOjMzOiJXZWJhdXRoblxUcnVzdFBhdGhcRW1wdHlUcnVzdFBhdGgiOjA6e31zOjk6IgAqAGFhZ3VpZCI7TzozNToiUmFtc2V5XFV1aWRcTGF6eVxMYXp5VXVpZEZyb21TdHJpbmciOjE6e3M6Njoic3RyaW5nIjtzOjM2OiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiO31zOjIyOiIAKgBjcmVkZW50aWFsUHVibGljS2V5IjtzOjc3OiKlAQIDJiABIVggHucXfQh0acwpsffVRM02F7P57mVm6hPX/l8Pjbh0jOwiWCBRT5MMqa909tcXHqG/EKfjXXDd9UEisk+ZF7QSTfwv0CI7czoxMzoiACoAdXNlckhhbmRsZSI7czoxNDoib3hkZWZhdWx0YWRtaW4iO3M6MTA6IgAqAGNvdW50ZXIiO2k6NDI3MTtzOjEwOiIAKgBvdGhlclVJIjtOO30=',
]
);
$this->createObject(
PublicKeyCredential::class,
$this->credentialList[2],
[
'oxuserid' => $this->userList[2],
'oxshopid' => 1,
'name' => __CLASS__,
'credentialid' => 'ITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAAA==',
'credential'=> 'TzozNDoiV2ViYXV0aG5cUHVibGljS2V5Q3JlZGVudGlhbFNvdXJjZSI6MTA6e3M6MjQ6IgAqAHB1YmxpY0tleUNyZWRlbnRpYWxJZCI7czo3MDoiITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAACI7czo3OiIAKgB0eXBlIjtzOjEwOiJwdWJsaWMta2V5IjtzOjEzOiIAKgB0cmFuc3BvcnRzIjthOjA6e31zOjE4OiIAKgBhdHRlc3RhdGlvblR5cGUiO3M6NDoibm9uZSI7czoxMjoiACoAdHJ1c3RQYXRoIjtPOjMzOiJXZWJhdXRoblxUcnVzdFBhdGhcRW1wdHlUcnVzdFBhdGgiOjA6e31zOjk6IgAqAGFhZ3VpZCI7TzozNToiUmFtc2V5XFV1aWRcTGF6eVxMYXp5VXVpZEZyb21TdHJpbmciOjE6e3M6Njoic3RyaW5nIjtzOjM2OiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiO31zOjIyOiIAKgBjcmVkZW50aWFsUHVibGljS2V5IjtzOjc3OiKlAQIDJiABIVggHucXfQh0acwpsffVRM02F7P57mVm6hPX/l8Pjbh0jOwiWCBRT5MMqa909tcXHqG/EKfjXXDd9UEisk+ZF7QSTfwv0CI7czoxMzoiACoAdXNlckhhbmRsZSI7czoxNDoib3hkZWZhdWx0YWRtaW4iO3M6MTA6IgAqAGNvdW50ZXIiO2k6NDI3MTtzOjEwOiIAKgBvdGhlclVJIjtOO30=',
]
);
$this->createObject(
PublicKeyCredential::class,
$this->credentialList[3],
[
'oxuserid' => $this->userList[3],
'oxshopid' => 1,
'name' => __CLASS__,
'credentialid' => 'ITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAAA==',
'credential'=> 'TzozNDoiV2ViYXV0aG5cUHVibGljS2V5Q3JlZGVudGlhbFNvdXJjZSI6MTA6e3M6MjQ6IgAqAHB1YmxpY0tleUNyZWRlbnRpYWxJZCI7czo3MDoiITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAACI7czo3OiIAKgB0eXBlIjtzOjEwOiJwdWJsaWMta2V5IjtzOjEzOiIAKgB0cmFuc3BvcnRzIjthOjA6e31zOjE4OiIAKgBhdHRlc3RhdGlvblR5cGUiO3M6NDoibm9uZSI7czoxMjoiACoAdHJ1c3RQYXRoIjtPOjMzOiJXZWJhdXRoblxUcnVzdFBhdGhcRW1wdHlUcnVzdFBhdGgiOjA6e31zOjk6IgAqAGFhZ3VpZCI7TzozNToiUmFtc2V5XFV1aWRcTGF6eVxMYXp5VXVpZEZyb21TdHJpbmciOjE6e3M6Njoic3RyaW5nIjtzOjM2OiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiO31zOjIyOiIAKgBjcmVkZW50aWFsUHVibGljS2V5IjtzOjc3OiKlAQIDJiABIVggHucXfQh0acwpsffVRM02F7P57mVm6hPX/l8Pjbh0jOwiWCBRT5MMqa909tcXHqG/EKfjXXDd9UEisk+ZF7QSTfwv0CI7czoxMzoiACoAdXNlckhhbmRsZSI7czoxNDoib3hkZWZhdWx0YWRtaW4iO3M6MTA6IgAqAGNvdW50ZXIiO2k6NDI3MTtzOjEwOiIAKgBvdGhlclVJIjtOO30=',
]
);
$this->createObject(
PublicKeyCredential::class,
$this->credentialList[4],
[
'oxuserid' => $this->userList[4],
'oxshopid' => 1,
'name' => __CLASS__,
'credentialid' => 'ITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAAA==',
'credential'=> 'TzozNDoiV2ViYXV0aG5cUHVibGljS2V5Q3JlZGVudGlhbFNvdXJjZSI6MTA6e3M6MjQ6IgAqAHB1YmxpY0tleUNyZWRlbnRpYWxJZCI7czo3MDoiITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAACI7czo3OiIAKgB0eXBlIjtzOjEwOiJwdWJsaWMta2V5IjtzOjEzOiIAKgB0cmFuc3BvcnRzIjthOjA6e31zOjE4OiIAKgBhdHRlc3RhdGlvblR5cGUiO3M6NDoibm9uZSI7czoxMjoiACoAdHJ1c3RQYXRoIjtPOjMzOiJXZWJhdXRoblxUcnVzdFBhdGhcRW1wdHlUcnVzdFBhdGgiOjA6e31zOjk6IgAqAGFhZ3VpZCI7TzozNToiUmFtc2V5XFV1aWRcTGF6eVxMYXp5VXVpZEZyb21TdHJpbmciOjE6e3M6Njoic3RyaW5nIjtzOjM2OiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiO31zOjIyOiIAKgBjcmVkZW50aWFsUHVibGljS2V5IjtzOjc3OiKlAQIDJiABIVggHucXfQh0acwpsffVRM02F7P57mVm6hPX/l8Pjbh0jOwiWCBRT5MMqa909tcXHqG/EKfjXXDd9UEisk+ZF7QSTfwv0CI7czoxMzoiACoAdXNlckhhbmRsZSI7czoxNDoib3hkZWZhdWx0YWRtaW4iO3M6MTA6IgAqAGNvdW50ZXIiO2k6NDI3MTtzOjEwOiIAKgBvdGhlclVJIjtOO30=',
]
);
$this->createObject(
PublicKeyCredential::class,
$this->credentialList[5],
[
'oxuserid' => $this->userList[5],
'oxshopid' => 2,
'name' => __CLASS__,
'credentialid' => 'ITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAAA==',
'credential'=> 'TzozNDoiV2ViYXV0aG5cUHVibGljS2V5Q3JlZGVudGlhbFNvdXJjZSI6MTA6e3M6MjQ6IgAqAHB1YmxpY0tleUNyZWRlbnRpYWxJZCI7czo3MDoiITSNkDRdN1bfRrb9MDCNOfBNay7YqT3ZxWxxqIQWVvwN0tFOG7SN2JiCfcUfPMBhE9bTLU1Gbb/8+5eHyFR2d5DCrxAAACI7czo3OiIAKgB0eXBlIjtzOjEwOiJwdWJsaWMta2V5IjtzOjEzOiIAKgB0cmFuc3BvcnRzIjthOjA6e31zOjE4OiIAKgBhdHRlc3RhdGlvblR5cGUiO3M6NDoibm9uZSI7czoxMjoiACoAdHJ1c3RQYXRoIjtPOjMzOiJXZWJhdXRoblxUcnVzdFBhdGhcRW1wdHlUcnVzdFBhdGgiOjA6e31zOjk6IgAqAGFhZ3VpZCI7TzozNToiUmFtc2V5XFV1aWRcTGF6eVxMYXp5VXVpZEZyb21TdHJpbmciOjE6e3M6Njoic3RyaW5nIjtzOjM2OiIwMDAwMDAwMC0wMDAwLTAwMDAtMDAwMC0wMDAwMDAwMDAwMDAiO31zOjIyOiIAKgBjcmVkZW50aWFsUHVibGljS2V5IjtzOjc3OiKlAQIDJiABIVggHucXfQh0acwpsffVRM02F7P57mVm6hPX/l8Pjbh0jOwiWCBRT5MMqa909tcXHqG/EKfjXXDd9UEisk+ZF7QSTfwv0CI7czoxMzoiACoAdXNlckhhbmRsZSI7czoxNDoib3hkZWZhdWx0YWRtaW4iO3M6MTA6IgAqAGNvdW50ZXIiO2k6NDI3MTtzOjEwOiIAKgBvdGhlclVJIjtOO30=',
]
);
}
public function cleanTestData()
{
parent::cleanTestData();
$this->deleteUser($this->userList[5]);
$this->deleteObject(PublicKeyCredential::class, $this->credentialList[1]);
$this->deleteObject(PublicKeyCredential::class, $this->credentialList[2]);
$this->deleteObject(PublicKeyCredential::class, $this->credentialList[3]);
$this->deleteObject(PublicKeyCredential::class, $this->credentialList[4]);
$this->deleteObject(PublicKeyCredential::class, $this->credentialList[5]);
}
/**
* @test
* @param $username
* @param $password
* @param $expected
* @param $redirect
* @return void
* @dataProvider loginDataProvider
*/
public function testCheckLoginReturn($username, $password, $expected, $redirect = null)
{
$utilsMock = $this->getMockBuilder(Utils::class)
->onlyMethods(['redirect'])
->getMock();
$utilsMock->expects($redirect ?: $this->never())->method('redirect')->willReturn(true);
d3DicHandler::getInstance()->set('d3ox.webauthn.'.Utils::class, $utilsMock);
parent::testCheckLoginReturn($username, $password, $expected);
}
/**
* @return array[]
*/
public function loginDataProvider(): array
{
return [
'not existing account' => ['unknown@user.localhost', '123456', 'user'],
'missing password' => ['noadmin@user.localhost', null, 'user', $this->once()],
'inactive account' => ['inactive@user.localhost', '123456', 'user'],
'wrong shop account' => ['wrongshop@user.localhost', '123456', 'user'],
'account ok' => ['admin@user.localhost', '123456', 'user'],
'cred. wrong shopid' => ['wawrongshopid@user.localhost', null, 'user'],
'credpass. wrong shopid'=> ['wawrongshopid@user.localhost', '123456', 'payment'],
];
}
}

View File

@ -15,6 +15,7 @@
<filter> <filter>
<whitelist processUncoveredFilesFromWhitelist="true"> <whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">../Application</directory> <directory suffix=".php">../Application</directory>
<directory suffix=".php">../migration</directory>
<directory suffix=".php">../Modules</directory> <directory suffix=".php">../Modules</directory>
<directory suffix=".php">../Setup</directory> <directory suffix=".php">../Setup</directory>
<exclude> <exclude>

View File

@ -13,6 +13,7 @@
namespace D3\Webauthn\tests\unit\Application\Controller\Admin; namespace D3\Webauthn\tests\unit\Application\Controller\Admin;
use Assert\InvalidArgumentException;
use D3\TestingTools\Development\CanAccessRestricted; use D3\TestingTools\Development\CanAccessRestricted;
use D3\TestingTools\Production\IsMockable; use D3\TestingTools\Production\IsMockable;
use D3\Webauthn\Application\Controller\Admin\d3user_webauthn; use D3\Webauthn\Application\Controller\Admin\d3user_webauthn;
@ -172,7 +173,7 @@ class d3user_webauthnTest extends WAUnitTestCase
]) ])
->getMock(); ->getMock();
$sutMock->expects($this->atLeastOnce())->method('setPageType'); $sutMock->expects($this->atLeastOnce())->method('setPageType');
$sutMock->expects($this->atLeastOnce())->method('setAuthnRegister')->willThrowException(oxNew(WebauthnException::class)); $sutMock->expects($this->atLeastOnce())->method('setAuthnRegister')->willThrowException(new InvalidArgumentException('msg', 20));
$this->callMethod( $this->callMethod(
$sutMock, $sutMock,

View File

@ -144,11 +144,15 @@ class d3webauthnadminloginTest extends d3webauthnloginTest
* @test * @test
* @return void * @return void
* @throws ReflectionException * @throws ReflectionException
* @dataProvider \D3\Webauthn\tests\unit\Application\Controller\d3webauthnloginTest::generateCredentialRequestFailedDataProvider()
* @covers \D3\Webauthn\Application\Controller\Admin\d3webauthnadminlogin::generateCredentialRequest * @covers \D3\Webauthn\Application\Controller\Admin\d3webauthnadminlogin::generateCredentialRequest
*/ */
public function generateCredentialRequestFailed($redirectClass = 'login', $userVarName = WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER) public function generateCredentialRequestFailed(
{ $exception,
parent::generateCredentialRequestFailed($redirectClass, $userVarName); $redirectClass = 'login',
$userVarName = WebauthnConf::WEBAUTHN_ADMIN_SESSION_CURRENTUSER
) {
parent::generateCredentialRequestFailed($exception, $redirectClass, $userVarName);
} }
/** /**

View File

@ -187,7 +187,7 @@ class d3_account_webauthnTest extends WAUnitTestCase
/** @var LoggerInterface|MockObject $loggerMock */ /** @var LoggerInterface|MockObject $loggerMock */
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']); $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']);
$loggerMock->expects($this->never())->method('error')->willReturn(true); $loggerMock->expects($this->never())->method('error')->willReturn(true);
$loggerMock->expects($this->never())->method('debug')->willReturn(true); $loggerMock->method('debug')->willReturn(true);
d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock); d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock);
/** @var d3_account_webauthn|MockObject $oControllerMock */ /** @var d3_account_webauthn|MockObject $oControllerMock */
@ -210,9 +210,10 @@ class d3_account_webauthnTest extends WAUnitTestCase
* @test * @test
* @return void * @return void
* @throws ReflectionException * @throws ReflectionException
* @dataProvider canRequestNewCredentialCantGetCreationOptionsDataProvider
* @covers \D3\Webauthn\Application\Controller\d3_account_webauthn::requestNewCredential() * @covers \D3\Webauthn\Application\Controller\d3_account_webauthn::requestNewCredential()
*/ */
public function canRequestNewCredentialCantGetCreationOptions() public function canRequestNewCredentialCantGetCreationOptions($exception)
{ {
$oUser = oxNew(User::class); $oUser = oxNew(User::class);
$oUser->setId('foo'); $oUser->setId('foo');
@ -225,7 +226,7 @@ class d3_account_webauthnTest extends WAUnitTestCase
/** @var LoggerInterface|MockObject $loggerMock */ /** @var LoggerInterface|MockObject $loggerMock */
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']); $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']);
$loggerMock->expects($this->atLeastOnce())->method('error')->willReturn(true); $loggerMock->expects($this->atLeastOnce())->method('error')->willReturn(true);
$loggerMock->expects($this->atLeastOnce())->method('debug')->willReturn(true); $loggerMock->method('debug')->willReturn(true);
d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock); d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock);
/** @var d3_account_webauthn|MockObject $oControllerMock */ /** @var d3_account_webauthn|MockObject $oControllerMock */
@ -233,7 +234,7 @@ class d3_account_webauthnTest extends WAUnitTestCase
->onlyMethods(['setAuthnRegister', 'setPageType', 'getUser']) ->onlyMethods(['setAuthnRegister', 'setPageType', 'getUser'])
->getMock(); ->getMock();
$oControllerMock->expects($this->atLeastOnce())->method('setAuthnRegister') $oControllerMock->expects($this->atLeastOnce())->method('setAuthnRegister')
->willThrowException(oxNew(WebauthnException::class)); ->willThrowException($exception);
$oControllerMock->expects($this->never())->method('setPageType'); $oControllerMock->expects($this->never())->method('setPageType');
$oControllerMock->method('getUser')->willReturn($oUser); $oControllerMock->method('getUser')->willReturn($oUser);
@ -245,6 +246,16 @@ class d3_account_webauthnTest extends WAUnitTestCase
); );
} }
/**
* @return Generator
*/
public function canRequestNewCredentialCantGetCreationOptionsDataProvider(): Generator
{
yield 'WebauthnException' => [oxNew(WebauthnException::class)];
yield 'InvalidArgumentException' => [oxNew(InvalidArgumentException::class, 'msg', 20)];
}
/** /**
* @test * @test
* @param $throwExc * @param $throwExc
@ -334,7 +345,7 @@ class d3_account_webauthnTest extends WAUnitTestCase
/** @var LoggerInterface|MockObject $loggerMock */ /** @var LoggerInterface|MockObject $loggerMock */
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']); $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']);
$loggerMock->expects($this->once())->method('error')->willReturn(true); $loggerMock->expects($this->once())->method('error')->willReturn(true);
$loggerMock->expects($this->once())->method('debug')->willReturn(true); $loggerMock->method('debug')->willReturn(true);
d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock); d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock);
/** @var User|MockObject $userMock */ /** @var User|MockObject $userMock */
@ -425,7 +436,7 @@ class d3_account_webauthnTest extends WAUnitTestCase
/** @var LoggerInterface|MockObject $loggerMock */ /** @var LoggerInterface|MockObject $loggerMock */
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']); $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']);
$loggerMock->expects($this->once())->method('error')->willReturn(true); $loggerMock->expects($this->once())->method('error')->willReturn(true);
$loggerMock->expects($this->once())->method('debug')->willReturn(true); $loggerMock->method('debug')->willReturn(true);
d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock); d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock);
/** @var User|MockObject $userMock */ /** @var User|MockObject $userMock */

View File

@ -15,6 +15,7 @@ declare(strict_types=1);
namespace D3\Webauthn\tests\unit\Application\Controller; namespace D3\Webauthn\tests\unit\Application\Controller;
use Assert\InvalidArgumentException;
use D3\TestingTools\Development\CanAccessRestricted; use D3\TestingTools\Development\CanAccessRestricted;
use D3\Webauthn\Application\Controller\d3webauthnlogin; use D3\Webauthn\Application\Controller\d3webauthnlogin;
use D3\Webauthn\Application\Model\Exceptions\WebauthnException; use D3\Webauthn\Application\Model\Exceptions\WebauthnException;
@ -144,7 +145,7 @@ class d3webauthnloginTest extends WAUnitTestCase
/** @var LoggerInterface|MockObject $loggerMock */ /** @var LoggerInterface|MockObject $loggerMock */
$loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']); $loggerMock = $this->getMockForAbstractClass(LoggerInterface::class, [], '', true, true, true, ['error', 'debug']);
$loggerMock->expects($this->never())->method('error')->willReturn(true); $loggerMock->expects($this->never())->method('error')->willReturn(true);
$loggerMock->expects($this->never())->method('debug')->willReturn(true); $loggerMock->method('debug')->willReturn(true);
d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock); d3GetOxidDIC()->set('d3ox.webauthn.'.LoggerInterface::class, $loggerMock);
/** @var Session|MockObject $sessionMock */ /** @var Session|MockObject $sessionMock */
@ -181,9 +182,14 @@ class d3webauthnloginTest extends WAUnitTestCase
* @test * @test
* @return void * @return void
* @throws ReflectionException * @throws ReflectionException
* @dataProvider generateCredentialRequestFailedDataProvider
* @covers \D3\Webauthn\Application\Controller\d3webauthnlogin::generateCredentialRequest * @covers \D3\Webauthn\Application\Controller\d3webauthnlogin::generateCredentialRequest
*/ */
public function generateCredentialRequestFailed($redirectClass = 'start', $userVarName = WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER) public function generateCredentialRequestFailed(
$exception,
$redirectClass = 'start',
$userVarName = WebauthnConf::WEBAUTHN_SESSION_CURRENTUSER
)
{ {
$currUserFixture = 'currentUserFixture'; $currUserFixture = 'currentUserFixture';
@ -209,7 +215,7 @@ class d3webauthnloginTest extends WAUnitTestCase
->onlyMethods(['getRequestOptions']) ->onlyMethods(['getRequestOptions'])
->getMock(); ->getMock();
$webAuthnMock->expects($this->once())->method('getRequestOptions')->with($currUserFixture) $webAuthnMock->expects($this->once())->method('getRequestOptions')->with($currUserFixture)
->willThrowException(oxNew(WebauthnException::class, 'foobar0')); ->willThrowException($exception);
d3GetOxidDIC()->set(Webauthn::class, $webAuthnMock); d3GetOxidDIC()->set(Webauthn::class, $webAuthnMock);
/** @var Utils|MockObject $utilsMock */ /** @var Utils|MockObject $utilsMock */
@ -233,6 +239,15 @@ class d3webauthnloginTest extends WAUnitTestCase
); );
} }
/**
* @return Generator
*/
public function generateCredentialRequestFailedDataProvider(): Generator
{
yield 'WebauthnException' => [oxNew(WebauthnException::class, 'foobar0')];
yield 'InvalidArgumentException' => [oxNew(InvalidArgumentException::class, 'foobar0', 20)];
}
/** /**
* @test * @test
* @return void * @return void

View File

@ -141,8 +141,8 @@ class PublicKeyCredentialTest extends WAUnitTestCase
$this->canGetField( $this->canGetField(
'credentialid', 'credentialid',
'getCredentialId', 'getCredentialId',
'credentialFixture', base64_encode('credentialFixture'),
base64_decode('credentialFixture') 'credentialFixture'
); );
} }

View File

@ -182,7 +182,7 @@ class WebauthnTest extends WAUnitTestCase
*/ */
public function canGetOptionsDataProvider(): Generator public function canGetOptionsDataProvider(): Generator
{ {
yield 'json encoded' => ['jsonstring']; yield 'json encoded' => [json_encode(['jsonstring'])];
yield 'json failed' => [false]; yield 'json failed' => [false];
} }

View File

@ -19,9 +19,9 @@ use D3\TestingTools\Development\CanAccessRestricted;
use D3\Webauthn\Setup\Actions; use D3\Webauthn\Setup\Actions;
use D3\Webauthn\tests\unit\WAUnitTestCase; use D3\Webauthn\tests\unit\WAUnitTestCase;
use Exception; use Exception;
use OxidEsales\DoctrineMigrationWrapper\Migrations;
use OxidEsales\DoctrineMigrationWrapper\MigrationsBuilder;
use OxidEsales\Eshop\Application\Controller\FrontendController; use OxidEsales\Eshop\Application\Controller\FrontendController;
use OxidEsales\Eshop\Core\Database\Adapter\DatabaseInterface;
use OxidEsales\Eshop\Core\Database\Adapter\Doctrine\Database;
use OxidEsales\Eshop\Core\DbMetaDataHandler; use OxidEsales\Eshop\Core\DbMetaDataHandler;
use OxidEsales\Eshop\Core\Registry; use OxidEsales\Eshop\Core\Registry;
use OxidEsales\Eshop\Core\SeoEncoder; use OxidEsales\Eshop\Core\SeoEncoder;
@ -45,141 +45,35 @@ class ActionsTest extends WAUnitTestCase
/** /**
* @test * @test
* @param $tableExist
* @param $expectedInvocation
* @return void * @return void
* @throws ReflectionException * @throws ReflectionException
* @covers \D3\Webauthn\Setup\Actions::setupModule * @covers \D3\Webauthn\Setup\Actions::runModuleMigrations()
* @dataProvider canSetupModuleDataProvider
*/ */
public function canSetupModule($tableExist, $expectedInvocation) public function canRunModuleMigrations()
{ {
/** @var Actions|MockObject $sut */ /** @var Migrations|MockObject $migrationMock */
$sut = $this->getMockBuilder(Actions::class) $migrationMock = $this->getMockBuilder(Migrations::class)
->onlyMethods(['tableExists', 'executeSQL'])
->getMock();
$sut->method('tableExists')->willReturn($tableExist);
$sut->expects($expectedInvocation)->method('executeSQL')->willReturn(true);
$this->callMethod(
$sut,
'setupModule'
);
}
/**
* @return array[]
*/
public function canSetupModuleDataProvider(): array
{
return [
'table exist' => [true, $this->never()],
'table not exist' => [false, $this->once()],
];
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Setup\Actions::tableExists
*/
public function canCheckTableExists()
{
$expected = true;
/** @var DbMetaDataHandler|MockObject $DbMetaDataMock */
$DbMetaDataMock = $this->getMockBuilder(DbMetaDataHandler::class)
->onlyMethods(['tableExists'])
->getMock();
$DbMetaDataMock->expects($this->once())->method('tableExists')->willReturn($expected);
d3GetOxidDIC()->set('d3ox.webauthn.'.DbMetaDataHandler::class, $DbMetaDataMock);
/** @var Actions $sut */
$sut = oxNew(Actions::class);
$this->assertSame(
$expected,
$this->callMethod(
$sut,
'tableExists',
['testTable']
)
);
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Setup\Actions::d3GetDb
*/
public function d3GetDbReturnsRightInstance()
{
$sut = oxNew(Actions::class);
$this->assertInstanceOf(
DatabaseInterface::class,
$this->callMethod(
$sut,
'd3GetDb'
)
);
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Setup\Actions::executeSQL
*/
public function canExecuteSQL()
{
/** @var Database|MockObject $dbMock */
$dbMock = $this->getMockBuilder(Database::class)
->onlyMethods(['execute']) ->onlyMethods(['execute'])
->disableOriginalConstructor()
->getMock(); ->getMock();
$dbMock->expects($this->once())->method('execute'); $migrationMock->expects($this->atLeastOnce())->method('execute')->with(
$this->identicalTo('migrations:migrate'),
$sut = $this->getMockBuilder(Actions::class) $this->identicalTo('d3webauthn')
->onlyMethods(['d3GetDb'])
->getMock();
$sut->method('d3GetDb')->willReturn($dbMock);
$this->callMethod(
$sut,
'executeSQL',
['query']
); );
}
/** /** @var MigrationsBuilder|MockObject $migrationsBuilderMock */
* @test $migrationsBuilderMock = $this->getMockBuilder(MigrationsBuilder::class)
* @return void ->onlyMethods(['build'])
* @throws ReflectionException
* @covers \D3\Webauthn\Setup\Actions::fieldExists
*/
public function canCheckFieldExists()
{
$expected = true;
/** @var DbMetaDataHandler|MockObject $DbMetaDataMock */
$DbMetaDataMock = $this->getMockBuilder(DbMetaDataHandler::class)
->onlyMethods(['fieldExists'])
->getMock(); ->getMock();
$DbMetaDataMock->expects($this->once())->method('fieldExists')->willReturn($expected); $migrationsBuilderMock->method('build')->willReturn($migrationMock);
d3GetOxidDIC()->set('d3ox.webauthn.'.DbMetaDataHandler::class, $DbMetaDataMock); d3GetOxidDIC()->set('d3ox.webauthn.'.MigrationsBuilder::class, $migrationsBuilderMock);
/** @var Actions $sut */ /** @var Actions $sut */
$sut = oxNew(Actions::class); $sut = oxNew(Actions::class);
$this->assertSame( $this->callMethod(
$expected, $sut,
$this->callMethod( 'runModuleMigrations'
$sut,
'fieldExists',
['testField', 'testTable']
)
); );
} }

View File

@ -0,0 +1,303 @@
<?php
/**
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*
* https://www.d3data.de
*
* @copyright (C) D3 Data Development (Inh. Thomas Dartsch)
* @author D3 Data Development - Daniel Seifert <info@shopmodule.com>
* @link https://www.oxidmodule.com
*/
declare(strict_types=1);
namespace D3\Webauthn\tests\unit\migration\data;
use D3\TestingTools\Development\CanAccessRestricted;
use D3\Webauthn\Migrations\Version20230209212939;
use D3\Webauthn\tests\unit\WAUnitTestCase;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Schema\AbstractSchemaManager;
use Doctrine\DBAL\Schema\Column;
use Doctrine\DBAL\Schema\MySqlSchemaManager;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\DBAL\Schema\Table;
use Doctrine\Migrations\Configuration\Configuration;
use Doctrine\Migrations\Version\Version;
use Generator;
use PHPUnit\Framework\MockObject\MockObject;
use ReflectionException;
class Version20230209212939Test extends WAUnitTestCase
{
use CanAccessRestricted;
/** @var Version20230209212939 */
protected $sut;
public function setUp(): void
{
parent::setUp();
/** @var AbstractPlatform|MockObject $databasePlatformMock */
$databasePlatformMock = $this->getMockBuilder(MySQL57Platform::class)
->getMock();
/** @var AbstractSchemaManager|MockObject $schemaManagerMock */
$schemaManagerMock = $this->getMockBuilder(MySqlSchemaManager::class)
->disableOriginalConstructor()
->getMock();
/** @var Connection|MockObject $connectionMock */
$connectionMock = $this->getMockBuilder(Connection::class)
->disableOriginalConstructor()
->onlyMethods(['getDatabasePlatform', 'getSchemaManager'])
->getMock();
$connectionMock->method('getDatabasePlatform')->willReturn($databasePlatformMock);
$connectionMock->method('getSchemaManager')->willReturn($schemaManagerMock);
/** @var Configuration|MockObject $configurationMock */
$configurationMock = $this->getMockBuilder(Configuration::class)
->disableOriginalConstructor()
->onlyMethods(['getConnection'])
->getMock();
$configurationMock->method('getConnection')->willReturn($connectionMock);
/** @var Version|MockObject $versionMock */
$versionMock = $this->getMockBuilder(Version::class)
->onlyMethods(['getConfiguration'])
->disableOriginalConstructor()
->getMock();
$versionMock->method('getConfiguration')->willReturn($configurationMock);
$this->sut = oxNew(Version20230209212939::class, $versionMock);
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::getDescription
*/
public function canGetDescription()
{
$this->assertIsString(
$this->callMethod(
$this->sut,
'getDescription'
)
);
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::up
* @dataProvider canUpTableDataProvider
*/
public function canUpTable($tableExist, $invocationCount)
{
/** @var Table|MockObject $tableMock */
$tableMock = $this->getMockBuilder(Table::class)
->onlyMethods(['hasColumn', 'hasPrimaryKey', 'hasIndex'])
->disableOriginalConstructor()
->getMock();
$tableMock->method('hasColumn')->willReturn(true);
$tableMock->method('hasPrimaryKey')->willReturn(true);
$tableMock->method('hasIndex')->willReturn(true);
/** @var Schema|MockObject $schemaMock */
$schemaMock = $this->getMockBuilder(Schema::class)
->onlyMethods(['hasTable', 'createTable', 'getTable'])
->getMock();
$schemaMock->method('hasTable')->willReturn($tableExist);
$schemaMock->expects($invocationCount)->method('createTable')->willReturn($tableMock);
$schemaMock->method('getTable')->willReturn($tableMock);
$this->callMethod(
$this->sut,
'up',
[$schemaMock]
);
}
/**
* @return Generator
*/
public function canUpTableDataProvider(): Generator
{
yield 'table not exist' => [false, $this->once()];
yield 'table exist' => [true, $this->never()];
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::up
* @dataProvider canUpColumnDataProvider
*/
public function canUpColumn($columnExist, $invocationCount)
{
/** @var Column|MockObject $columnMock */
$columnMock = $this->getMockBuilder(Column::class)
->onlyMethods(['setLength'])
->disableOriginalConstructor()
->getMock();
$columnMock->method('setLength')->willReturnSelf();
/** @var Table|MockObject $tableMock */
$tableMock = $this->getMockBuilder(Table::class)
->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex'])
->disableOriginalConstructor()
->getMock();
$tableMock->method('hasColumn')->willReturn($columnExist);
$tableMock->expects($invocationCount)->method('addColumn')->willReturn($columnMock);
$tableMock->method('hasPrimaryKey')->willReturn(true);
$tableMock->method('hasIndex')->willReturn(true);
/** @var Schema|MockObject $schemaMock */
$schemaMock = $this->getMockBuilder(Schema::class)
->onlyMethods(['hasTable', 'getTable'])
->getMock();
$schemaMock->method('hasTable')->willReturn(true);
$schemaMock->method('getTable')->willReturn($tableMock);
$this->callMethod(
$this->sut,
'up',
[$schemaMock]
);
}
/**
* @return Generator
*/
public function canUpColumnDataProvider(): Generator
{
yield 'column not exist' => [false, $this->atLeast(7)];
yield 'column exist' => [true, $this->never()];
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::up
* @dataProvider canUpPrimaryKeyDataProvider
*/
public function canUpPrimaryKey($pKeyExist, $invocationCount)
{
/** @var Table|MockObject $tableMock */
$tableMock = $this->getMockBuilder(Table::class)
->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex', 'setPrimaryKey'])
->disableOriginalConstructor()
->getMock();
$tableMock->method('hasColumn')->willReturn(true);
$tableMock->method('hasPrimaryKey')->willReturn($pKeyExist);
$tableMock->method('hasIndex')->willReturn(true);
$tableMock->expects($invocationCount)->method('setPrimaryKey');
/** @var Schema|MockObject $schemaMock */
$schemaMock = $this->getMockBuilder(Schema::class)
->onlyMethods(['hasTable', 'getTable'])
->getMock();
$schemaMock->method('hasTable')->willReturn(true);
$schemaMock->method('getTable')->willReturn($tableMock);
$this->callMethod(
$this->sut,
'up',
[$schemaMock]
);
}
/**
* @return Generator
*/
public function canUpPrimaryKeyDataProvider(): Generator
{
yield 'pk not exist' => [false, $this->once()];
yield 'pk exist' => [true, $this->never()];
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::up
* @dataProvider canUpIndexDataProvider
*/
public function canUpIndex($indexExist, $invocationCount)
{
/** @var Table|MockObject $tableMock */
$tableMock = $this->getMockBuilder(Table::class)
->onlyMethods(['hasColumn', 'addColumn', 'hasPrimaryKey', 'hasIndex', 'addIndex', 'setComment'])
->disableOriginalConstructor()
->getMock();
$tableMock->method('hasColumn')->willReturn(true);
$tableMock->method('hasPrimaryKey')->willReturn(true);
$tableMock->method('hasIndex')->willReturn($indexExist);
$tableMock->expects($invocationCount)->method('addIndex');
$tableMock->expects($this->once())->method('setComment');
/** @var Schema|MockObject $schemaMock */
$schemaMock = $this->getMockBuilder(Schema::class)
->onlyMethods(['hasTable', 'getTable'])
->getMock();
$schemaMock->method('hasTable')->willReturn(true);
$schemaMock->method('getTable')->willReturn($tableMock);
$this->callMethod(
$this->sut,
'up',
[$schemaMock]
);
}
/**
* @return Generator
*/
public function canUpIndexDataProvider(): Generator
{
yield 'index not exist' => [false, $this->atLeast(2)];
yield 'index exist' => [true, $this->never()];
}
/**
* @test
* @return void
* @throws ReflectionException
* @covers \D3\Webauthn\Migrations\Version20230209212939::down
* @dataProvider canDownTableDataProvider
*/
public function canDownTable($tableExist, $invocationCount)
{
/** @var Schema|MockObject $schemaMock */
$schemaMock = $this->getMockBuilder(Schema::class)
->onlyMethods(['hasTable', 'dropTable'])
->getMock();
$schemaMock->method('hasTable')->willReturn($tableExist);
$schemaMock->expects($invocationCount)->method('dropTable');
$this->callMethod(
$this->sut,
'down',
[$schemaMock]
);
}
/**
* @return Generator
*/
public function canDownTableDataProvider(): Generator
{
yield 'table exist' => [true, $this->once()];
yield 'table not exist' => [false, $this->never()];
}
}

8
wishlist.md Normal file
View File

@ -0,0 +1,8 @@
# Wish list for future releases
- a more intuitive login process (instead of simply having to leave the password field blank)
- forcing the user to use Webauthn
- General avoidance of passwords, login exclusively with FIDO2
- However, a restore strategy is required in the event that a key is no longer available.
- Alternatively, a random password unknown to the customer can be set, which is changed each time the customer logs on via Webauthn.
- Implementation of resident keys for logging in completely without user input (no user name required any more)