mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-07-24 04:44:35 +02:00
Merge branch 'webauthn_tfa'
This commit is contained in:
commit
27bf7c3be9
29 changed files with 1967 additions and 753 deletions
|
@ -1,134 +0,0 @@
|
|||
import u2fApi from 'u2f-api'
|
||||
|
||||
'use strict'
|
||||
|
||||
window.u2fauth = window.u2fauth || {}
|
||||
|
||||
u2fauth.formId = 'u2fForm'
|
||||
u2fauth.authCodeId = '_auth_code'
|
||||
u2fauth.keynameId = 'u2fkeyname'
|
||||
u2fauth.pressButtonId = 'u2fpressbutton'
|
||||
u2fauth.errorId = 'u2fError'
|
||||
u2fauth.timeout = 30
|
||||
u2fauth.errorTranslation = {
|
||||
1: 'Unknown Error',
|
||||
2: 'Bad Request',
|
||||
3: 'Client configuration not supported',
|
||||
4: 'Device already registered or ineligible',
|
||||
5: 'Timeout. Click to retry'
|
||||
}
|
||||
|
||||
u2fauth.ready = function (fn) {
|
||||
if (document.readyState !== 'loading') {
|
||||
fn()
|
||||
} else if (document.addEventListener) {
|
||||
document.addEventListener('DOMContentLoaded', fn)
|
||||
} else {
|
||||
document.attachEvent('onreadystatechange', function () {
|
||||
if (document.readyState !== 'loading') { fn() }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
u2fauth.authenticate = function () {
|
||||
u2fauth.clearError()
|
||||
u2fauth.showPressButton()
|
||||
|
||||
var form = document.getElementById(u2fauth.formId)
|
||||
var request = JSON.parse(form.dataset.request)
|
||||
|
||||
u2fApi.isSupported()
|
||||
.then(function (supported) {
|
||||
if (supported) {
|
||||
return u2fApi.sign(request, u2fauth.timeout)
|
||||
.then(response => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.submit(form, response)
|
||||
})
|
||||
} else {
|
||||
alert('Browser not supported')
|
||||
}
|
||||
})
|
||||
.catch(data => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.showError(data.metaData.code, u2fauth.authenticate)
|
||||
})
|
||||
}
|
||||
|
||||
u2fauth.register = function () {
|
||||
u2fauth.clearError()
|
||||
u2fauth.hideKeyname()
|
||||
u2fauth.showPressButton()
|
||||
|
||||
var form = document.getElementById(u2fauth.formId)
|
||||
var request = JSON.parse(form.dataset.request)
|
||||
|
||||
u2fApi.isSupported()
|
||||
.then(function (supported) {
|
||||
if (supported) {
|
||||
return u2fApi.register(request[0], request[1], u2fauth.timeout)
|
||||
.then(response => {
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.submit(form, response)
|
||||
})
|
||||
} else {
|
||||
alert('Browser not supported')
|
||||
}
|
||||
})
|
||||
.catch(data => {
|
||||
console.info(data)
|
||||
u2fauth.hidePressButton()
|
||||
u2fauth.showError(data.metaData.code, u2fauth.register)
|
||||
})
|
||||
}
|
||||
|
||||
u2fauth.submit = function (form, data) {
|
||||
var codeField = document.getElementById(u2fauth.authCodeId)
|
||||
codeField.value = JSON.stringify(data)
|
||||
form.submit()
|
||||
}
|
||||
|
||||
u2fauth.hideKeyname = function () {
|
||||
var keyname = document.getElementById(u2fauth.keynameId)
|
||||
keyname.style.display = 'none'
|
||||
}
|
||||
|
||||
u2fauth.hidePressButton = function () {
|
||||
var pressButton = document.getElementById(u2fauth.pressButtonId)
|
||||
pressButton.style.display = 'none'
|
||||
}
|
||||
|
||||
u2fauth.showPressButton = function () {
|
||||
var pressButton = document.getElementById(u2fauth.pressButtonId)
|
||||
pressButton.style.display = 'block'
|
||||
}
|
||||
|
||||
u2fauth.clearError = function () {
|
||||
var errorDisplay = document.getElementById(u2fauth.errorId)
|
||||
errorDisplay.style.display = 'none'
|
||||
errorDisplay.innerText = ''
|
||||
}
|
||||
|
||||
u2fauth.showError = function (error, callback) {
|
||||
var errorDisplay = document.getElementById(u2fauth.errorId)
|
||||
errorDisplay.style.display = 'block'
|
||||
errorDisplay.innerText = u2fauth.errorTranslation[error]
|
||||
errorDisplay.onclick = callback
|
||||
}
|
||||
|
||||
u2fauth.ready(function () {
|
||||
const form = document.getElementById('u2fForm')
|
||||
if (!form) {
|
||||
return
|
||||
}
|
||||
const type = form.dataset.action
|
||||
|
||||
if (type === 'auth') {
|
||||
u2fauth.authenticate()
|
||||
} else if (type === 'reg' && form.addEventListener) {
|
||||
form.addEventListener('submit', function (event) {
|
||||
event.preventDefault()
|
||||
u2fauth.register()
|
||||
}, false)
|
||||
}
|
||||
})
|
208
assets/js/webauthn_tfa.js
Normal file
208
assets/js/webauthn_tfa.js
Normal file
|
@ -0,0 +1,208 @@
|
|||
'use strict'
|
||||
|
||||
class WebauthnTFA {
|
||||
|
||||
// Decodes a Base64Url string
|
||||
_base64UrlDecode = (input) => {
|
||||
input = input
|
||||
.replace(/-/g, '+')
|
||||
.replace(/_/g, '/');
|
||||
|
||||
const pad = input.length % 4;
|
||||
if (pad) {
|
||||
if (pad === 1) {
|
||||
throw new Error('InvalidLengthError: Input base64url string is the wrong length to determine padding');
|
||||
}
|
||||
input += new Array(5-pad).join('=');
|
||||
}
|
||||
|
||||
return window.atob(input);
|
||||
};
|
||||
|
||||
// Converts an array of bytes into a Base64Url string
|
||||
_arrayToBase64String = (a) => btoa(String.fromCharCode(...a));
|
||||
|
||||
// Prepares the public key options object returned by the Webauthn Framework
|
||||
_preparePublicKeyOptions = publicKey => {
|
||||
//Convert challenge from Base64Url string to Uint8Array
|
||||
publicKey.challenge = Uint8Array.from(
|
||||
this._base64UrlDecode(publicKey.challenge),
|
||||
c => c.charCodeAt(0)
|
||||
);
|
||||
|
||||
//Convert the user ID from Base64 string to Uint8Array
|
||||
if (publicKey.user !== undefined) {
|
||||
publicKey.user = {
|
||||
...publicKey.user,
|
||||
id: Uint8Array.from(
|
||||
window.atob(publicKey.user.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
//If excludeCredentials is defined, we convert all IDs to Uint8Array
|
||||
if (publicKey.excludeCredentials !== undefined) {
|
||||
publicKey.excludeCredentials = publicKey.excludeCredentials.map(
|
||||
data => {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (publicKey.allowCredentials !== undefined) {
|
||||
publicKey.allowCredentials = publicKey.allowCredentials.map(
|
||||
data => {
|
||||
return {
|
||||
...data,
|
||||
id: Uint8Array.from(
|
||||
this._base64UrlDecode(data.id),
|
||||
c => c.charCodeAt(0)
|
||||
),
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return publicKey;
|
||||
};
|
||||
|
||||
// Prepares the public key credentials object returned by the authenticator
|
||||
_preparePublicKeyCredentials = data => {
|
||||
const publicKeyCredential = {
|
||||
id: data.id,
|
||||
type: data.type,
|
||||
rawId: this._arrayToBase64String(new Uint8Array(data.rawId)),
|
||||
response: {
|
||||
clientDataJSON: this._arrayToBase64String(
|
||||
new Uint8Array(data.response.clientDataJSON)
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
if (data.response.attestationObject !== undefined) {
|
||||
publicKeyCredential.response.attestationObject = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.attestationObject)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.authenticatorData !== undefined) {
|
||||
publicKeyCredential.response.authenticatorData = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.authenticatorData)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.signature !== undefined) {
|
||||
publicKeyCredential.response.signature = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.signature)
|
||||
);
|
||||
}
|
||||
|
||||
if (data.response.userHandle !== undefined) {
|
||||
publicKeyCredential.response.userHandle = this._arrayToBase64String(
|
||||
new Uint8Array(data.response.userHandle)
|
||||
);
|
||||
}
|
||||
|
||||
return publicKeyCredential;
|
||||
};
|
||||
|
||||
|
||||
constructor()
|
||||
{
|
||||
const register_dom_ready = (fn) => {
|
||||
document.addEventListener('turbo:load', fn)
|
||||
}
|
||||
|
||||
register_dom_ready(() => {
|
||||
this.registerForms();
|
||||
});
|
||||
}
|
||||
|
||||
registerForms()
|
||||
{
|
||||
//Find all forms which have an data-webauthn-tfa-action attribute
|
||||
const forms = document.querySelectorAll('form[data-webauthn-tfa-action]');
|
||||
|
||||
forms.forEach((form) => {
|
||||
console.debug('Found webauthn TFA form with action: ' + form.getAttribute('data-webauthn-tfa-action'), form);
|
||||
//Ensure that the form has webauthn data
|
||||
|
||||
const dataString = form.getAttribute('data-webauthn-tfa-data')
|
||||
const action = form.getAttribute('data-webauthn-tfa-action');
|
||||
|
||||
if (!dataString) {
|
||||
console.error('Form does not have webauthn data, can not continue!', form);
|
||||
return;
|
||||
}
|
||||
|
||||
//Convert dataString to the needed dataObject
|
||||
const dataObject = JSON.parse(dataString);
|
||||
const options = this._preparePublicKeyOptions(dataObject);
|
||||
|
||||
|
||||
if(action === 'authenticate'){
|
||||
this.authenticate(form, {publicKey: options});
|
||||
}
|
||||
|
||||
if(action === 'register'){
|
||||
//Register submit action, so we can do the registration on submit
|
||||
form.addEventListener('submit', (e) => {
|
||||
e.preventDefault();
|
||||
this.register(form, {publicKey: options});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form with the given result data
|
||||
* @param form
|
||||
* @param data
|
||||
* @private
|
||||
*/
|
||||
_submit(form, data)
|
||||
{
|
||||
const resultField = document.getElementById('_auth_code');
|
||||
resultField.value = JSON.stringify(data)
|
||||
form.submit();
|
||||
}
|
||||
|
||||
authenticate(form, authData)
|
||||
{
|
||||
navigator.credentials.get(authData)
|
||||
.then((credential) => {
|
||||
//Convert our credential to a form which can be JSON encoded
|
||||
let data = this._preparePublicKeyCredentials(credential);
|
||||
|
||||
this._submit(form, data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("WebAuthn Authentication error: ", error);
|
||||
alert("Error: " + error)
|
||||
});
|
||||
}
|
||||
|
||||
register(form, authData)
|
||||
{
|
||||
navigator.credentials.create(authData)
|
||||
.then((credential) => {
|
||||
//Convert our credential to a form which can be JSON encoded
|
||||
let data = this._preparePublicKeyCredentials(credential);
|
||||
|
||||
this._submit(form, data)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("WebAuthn Registration error: ", error);
|
||||
alert("Error: " + error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
window.webauthnTFA = new WebauthnTFA();
|
|
@ -10,7 +10,7 @@
|
|||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"beberlei/doctrineextensions": "^1.2",
|
||||
"brick/math": "^0.10.2",
|
||||
"brick/math": "^0.8.15",
|
||||
"composer/package-versions-deprecated": "1.11.99.4",
|
||||
"doctrine/annotations": "^1.6",
|
||||
"doctrine/doctrine-bundle": "^2.0",
|
||||
|
@ -21,6 +21,7 @@
|
|||
"florianv/swap": "^4.0",
|
||||
"florianv/swap-bundle": "dev-master",
|
||||
"gregwar/captcha-bundle": "^2.1.0",
|
||||
"jbtronics/2fa-webauthn": "^1.0.0",
|
||||
"league/html-to-markdown": "^5.0.1",
|
||||
"liip/imagine-bundle": "^2.2",
|
||||
"nelmio/security-bundle": "^3.0",
|
||||
|
@ -69,8 +70,8 @@
|
|||
"twig/inky-extra": "^3.0",
|
||||
"twig/intl-extra": "^3.0",
|
||||
"twig/markdown-extra": "^3.0",
|
||||
"webmozart/assert": "^1.4",
|
||||
"r/u2f-two-factor-bundle": "dev-scheb/2fa-support"
|
||||
"web-auth/webauthn-symfony-bundle": "^3.3",
|
||||
"webmozart/assert": "^1.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"dama/doctrine-test-bundle": "^7.0",
|
||||
|
|
1766
composer.lock
generated
1766
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -23,6 +23,8 @@ return [
|
|||
Florianv\SwapBundle\FlorianvSwapBundle::class => ['all' => true],
|
||||
Nelmio\SecurityBundle\NelmioSecurityBundle::class => ['all' => true],
|
||||
Symfony\UX\Turbo\TurboBundle::class => ['all' => true],
|
||||
Jbtronics\TFAWebauthn\TFAWebauthnBundle::class => ['all' => true],
|
||||
Scheb\TwoFactorBundle\SchebTwoFactorBundle::class => ['all' => true],
|
||||
R\U2FTwoFactorBundle\RU2FTwoFactorBundle::class => ['all' => true],
|
||||
SpomkyLabs\CborBundle\SpomkyLabsCborBundle::class => ['all' => true],
|
||||
Webauthn\Bundle\WebauthnBundle::class => ['all' => true],
|
||||
];
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
ru2_f_two_factor:
|
||||
formTemplate: "/security/U2F/u2f_login.html.twig"
|
||||
registerTemplate: "/security/U2F/u2f_register.html.twig"
|
||||
authCodeParameter: _auth_code
|
|
@ -25,4 +25,4 @@ scheb_two_factor:
|
|||
# If you're using guard-based authentication, you have to use this one:
|
||||
# - Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken
|
||||
# If you're using authenticator-based security (introduced in Symfony 5.1), you have to use this one:
|
||||
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
||||
# - Symfony\Component\Security\Http\Authenticator\Token\PostAuthenticationToken
|
5
config/packages/webauthn_2fa.yaml
Normal file
5
config/packages/webauthn_2fa.yaml
Normal file
|
@ -0,0 +1,5 @@
|
|||
tfa_webauthn:
|
||||
enabled: true
|
||||
template: 'Security/Webauthn/webauthn_login.html.twig'
|
||||
|
||||
rpName: '%partdb.title%'
|
|
@ -4,8 +4,4 @@
|
|||
_controller: "scheb_two_factor.form_controller::form"
|
||||
|
||||
2fa_login_check:
|
||||
path: /{_locale}/2fa_check
|
||||
|
||||
r_u2f_register:
|
||||
resource: "@RU2FTwoFactorBundle/Resources/config/routing.yml"
|
||||
prefix: /{_locale}/user
|
||||
path: /{_locale}/2fa_check
|
50
migrations/Version20221003212851.php
Normal file
50
migrations/Version20221003212851.php
Normal file
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace DoctrineMigrations;
|
||||
|
||||
use App\Migration\AbstractMultiPlatformMigration;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\Migrations\AbstractMigration;
|
||||
|
||||
/**
|
||||
* Auto-generated Migration: Please modify to your needs!
|
||||
*/
|
||||
final class Version20221003212851 extends AbstractMultiPlatformMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add Tables for Webauthn Keys';
|
||||
}
|
||||
|
||||
public function mySQLUp(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('CREATE TABLE webauthn_keys (id INT AUTO_INCREMENT NOT NULL, user_id INT DEFAULT NULL, public_key_credential_id LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', type VARCHAR(255) NOT NULL, transports LONGTEXT NOT NULL COMMENT \'(DC2Type:array)\', attestation_type VARCHAR(255) NOT NULL, trust_path LONGTEXT NOT NULL COMMENT \'(DC2Type:trust_path)\', aaguid TINYTEXT NOT NULL COMMENT \'(DC2Type:aaguid)\', credential_public_key LONGTEXT NOT NULL COMMENT \'(DC2Type:base64)\', user_handle VARCHAR(255) NOT NULL, counter INT NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, INDEX IDX_799FD143A76ED395 (user_id), PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB');
|
||||
$this->addSql('ALTER TABLE webauthn_keys ADD CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES `users` (id)');
|
||||
}
|
||||
|
||||
public function mySQLDown(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE webauthn_keys DROP FOREIGN KEY FK_799FD143A76ED395');
|
||||
$this->addSql('DROP TABLE webauthn_keys');
|
||||
}
|
||||
|
||||
public function sqLiteUp(Schema $schema): void
|
||||
{
|
||||
$this->addSql('CREATE TABLE webauthn_keys (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user_id INTEGER DEFAULT NULL, public_key_credential_id CLOB NOT NULL --(DC2Type:base64)
|
||||
, type VARCHAR(255) NOT NULL, transports CLOB NOT NULL --(DC2Type:array)
|
||||
, attestation_type VARCHAR(255) NOT NULL, trust_path CLOB NOT NULL --(DC2Type:trust_path)
|
||||
, aaguid CLOB NOT NULL --(DC2Type:aaguid)
|
||||
, credential_public_key CLOB NOT NULL --(DC2Type:base64)
|
||||
, user_handle VARCHAR(255) NOT NULL, counter INTEGER NOT NULL, name VARCHAR(255) NOT NULL, last_modified DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL, CONSTRAINT FK_799FD143A76ED395 FOREIGN KEY (user_id) REFERENCES "users" (id) NOT DEFERRABLE INITIALLY IMMEDIATE)');
|
||||
$this->addSql('CREATE INDEX IDX_799FD143A76ED395 ON webauthn_keys (user_id)');
|
||||
}
|
||||
|
||||
public function sqLiteDown(Schema $schema): void
|
||||
{
|
||||
$this->addSql('DROP TABLE webauthn_keys');
|
||||
}
|
||||
}
|
|
@ -113,7 +113,10 @@ class UserController extends AdminPages\BaseAdminController
|
|||
$entity->setGoogleAuthenticatorSecret(null);
|
||||
$entity->setBackupCodes([]);
|
||||
//Remove all U2F keys
|
||||
foreach ($entity->getU2FKeys() as $key) {
|
||||
foreach ($entity->getLegacyU2FKeys() as $key) {
|
||||
$em->remove($key);
|
||||
}
|
||||
foreach ($entity->getWebAuthnKeys() as $key) {
|
||||
$em->remove($key);
|
||||
}
|
||||
//Invalidate trusted devices
|
||||
|
|
|
@ -44,6 +44,7 @@ namespace App\Controller;
|
|||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
use App\Form\TFAGoogleSettingsType;
|
||||
|
@ -130,6 +131,7 @@ class UserSettingsController extends AbstractController
|
|||
}
|
||||
|
||||
if ($this->isCsrfTokenValid('delete'.$user->getId(), $request->request->get('_token'))) {
|
||||
//Handle U2F key removal
|
||||
if ($request->request->has('key_id')) {
|
||||
$key_id = $request->request->get('key_id');
|
||||
$key_repo = $entityManager->getRepository(U2FKey::class);
|
||||
|
@ -138,14 +140,14 @@ class UserSettingsController extends AbstractController
|
|||
if (null === $u2f) {
|
||||
$this->addFlash('danger', 'tfa_u2f.u2f_delete.not_existing');
|
||||
|
||||
throw new RuntimeException('Key not existing!');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
//User can only delete its own U2F keys
|
||||
if ($u2f->getUser() !== $user) {
|
||||
$this->addFlash('danger', 'tfa_u2f.u2f_delete.access_denied');
|
||||
|
||||
throw new RuntimeException('You can only delete your own U2F keys!');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
$backupCodeManager->disableBackupCodesIfUnused($user);
|
||||
|
@ -153,6 +155,31 @@ class UserSettingsController extends AbstractController
|
|||
$entityManager->flush();
|
||||
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
|
||||
} else if ($request->request->has('webauthn_key_id')) {
|
||||
$key_id = $request->request->get('webauthn_key_id');
|
||||
$key_repo = $entityManager->getRepository(WebauthnKey::class);
|
||||
/** @var WebauthnKey|null $key */
|
||||
$key = $key_repo->find($key_id);
|
||||
if (null === $key) {
|
||||
$this->addFlash('error', 'tfa_u2f.u2f_delete.not_existing');
|
||||
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
//User can only delete its own U2F keys
|
||||
if ($key->getUser() !== $user) {
|
||||
$this->addFlash('error', 'tfa_u2f.u2f_delete.access_denied');
|
||||
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
$backupCodeManager->disableBackupCodesIfUnused($user);
|
||||
$entityManager->remove($key);
|
||||
$entityManager->flush();
|
||||
$this->addFlash('success', 'tfa.u2f.u2f_delete.success');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_REMOVED);
|
||||
}
|
||||
|
|
60
src/Controller/WebauthnKeyRegistrationController.php
Normal file
60
src/Controller/WebauthnKeyRegistrationController.php
Normal file
|
@ -0,0 +1,60 @@
|
|||
<?php
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Jbtronics\TFAWebauthn\Services\TFAWebauthnRegistrationHelper;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
use function Symfony\Component\Translation\t;
|
||||
|
||||
class WebauthnKeyRegistrationController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* @Route("/webauthn/register", name="webauthn_register")
|
||||
*/
|
||||
public function register(Request $request, TFAWebauthnRegistrationHelper $registrationHelper, EntityManagerInterface $em)
|
||||
{
|
||||
|
||||
//If form was submitted, check the auth response
|
||||
if ($request->getMethod() === 'POST') {
|
||||
$webauthnResponse = $request->request->get('_auth_code');
|
||||
|
||||
//Retrieve other data from the form, that you want to store with the key
|
||||
$keyName = $request->request->get('keyName');
|
||||
if (empty($keyName)) {
|
||||
$keyName = 'Key ' . date('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
//Check the response
|
||||
try {
|
||||
$new_key = $registrationHelper->checkRegistrationResponse($webauthnResponse);
|
||||
} catch (\Exception $exception) {
|
||||
$this->addFlash('error', t('tfa_u2f.add_key.registration_error'));
|
||||
return $this->redirectToRoute('webauthn_register');
|
||||
}
|
||||
|
||||
$keyEntity = WebauthnKey::fromRegistration($new_key);
|
||||
$keyEntity->setName($keyName);
|
||||
$keyEntity->setUser($this->getUser());
|
||||
|
||||
$em->persist($keyEntity);
|
||||
$em->flush();
|
||||
|
||||
|
||||
$this->addFlash('success', 'Key registered successfully');
|
||||
return $this->redirectToRoute('user_settings');
|
||||
}
|
||||
|
||||
|
||||
return $this->render(
|
||||
'Security/Webauthn/webauthn_register.html.twig',
|
||||
[
|
||||
'registrationRequest' => $registrationHelper->generateRegistrationRequestAsJSON(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
|
@ -44,8 +44,7 @@ namespace App\Entity\UserSystem;
|
|||
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
|
||||
use u2flib_server\Registration;
|
||||
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
|
@ -56,7 +55,7 @@ use u2flib_server\Registration;
|
|||
* })
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class U2FKey implements TwoFactorKeyInterface
|
||||
class U2FKey implements LegacyU2FKeyInterface
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
|
@ -110,14 +109,6 @@ class U2FKey implements TwoFactorKeyInterface
|
|||
**/
|
||||
protected ?User $user = null;
|
||||
|
||||
public function fromRegistrationData(Registration $data): void
|
||||
{
|
||||
$this->keyHandle = $data->keyHandle;
|
||||
$this->publicKey = $data->publicKey;
|
||||
$this->certificate = $data->certificate;
|
||||
$this->counter = $data->counter;
|
||||
}
|
||||
|
||||
public function getKeyHandle(): string
|
||||
{
|
||||
return $this->keyHandle;
|
||||
|
|
|
@ -57,7 +57,9 @@ use App\Entity\PriceInformations\Currency;
|
|||
use App\Security\Interfaces\HasPermissionsInterface;
|
||||
use App\Validator\Constraints\Selectable;
|
||||
use App\Validator\Constraints\ValidPermission;
|
||||
use Jbtronics\TFAWebauthn\Model\LegacyU2FKeyInterface;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Webauthn\PublicKeyCredentialUserEntity;
|
||||
use function count;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
|
@ -65,8 +67,6 @@ use Doctrine\Common\Collections\Collection;
|
|||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Exception;
|
||||
use function in_array;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorInterface as U2FTwoFactorInterface;
|
||||
use R\U2FTwoFactorBundle\Model\U2F\TwoFactorKeyInterface;
|
||||
use Scheb\TwoFactorBundle\Model\BackupCodeInterface;
|
||||
use Scheb\TwoFactorBundle\Model\Google\TwoFactorInterface;
|
||||
use Scheb\TwoFactorBundle\Model\PreferredProviderInterface;
|
||||
|
@ -74,6 +74,7 @@ use Scheb\TwoFactorBundle\Model\TrustedDeviceInterface;
|
|||
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
use Jbtronics\TFAWebauthn\Model\TwoFactorInterface as WebauthnTwoFactorInterface;
|
||||
|
||||
/**
|
||||
* This entity represents a user, which can log in and have permissions.
|
||||
|
@ -86,7 +87,7 @@ use Symfony\Component\Validator\Constraints as Assert;
|
|||
* @ORM\EntityListeners({"App\EntityListeners\TreeCacheInvalidationListener"})
|
||||
* @UniqueEntity("name", message="validator.user.username_already_used")
|
||||
*/
|
||||
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, U2FTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface
|
||||
class User extends AttachmentContainingDBElement implements UserInterface, HasPermissionsInterface, TwoFactorInterface, BackupCodeInterface, TrustedDeviceInterface, WebauthnTwoFactorInterface, PreferredProviderInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
//use MasterAttachmentTrait;
|
||||
|
||||
|
@ -241,11 +242,17 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
*/
|
||||
protected ?DateTime $backupCodesGenerationDate = null;
|
||||
|
||||
/** @var Collection<int, TwoFactorKeyInterface>
|
||||
/** @var Collection<int, LegacyU2FKeyInterface>
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserSystem\U2FKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true)
|
||||
*/
|
||||
protected $u2fKeys;
|
||||
|
||||
/**
|
||||
* @var Collection<int, WebauthnKey>
|
||||
* @ORM\OneToMany(targetEntity="App\Entity\UserSystem\WebauthnKey", mappedBy="user", cascade={"REMOVE"}, orphanRemoval=true)
|
||||
*/
|
||||
protected $webauthn_keys;
|
||||
|
||||
/**
|
||||
* @var Currency|null The currency the user wants to see prices in.
|
||||
* Dont use fetch=EAGER here, this will cause problems with setting the currency setting.
|
||||
|
@ -274,6 +281,7 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
parent::__construct();
|
||||
$this->permissions = new PermissionsEmbed();
|
||||
$this->u2fKeys = new ArrayCollection();
|
||||
$this->webauthn_keys = new ArrayCollection();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -838,48 +846,48 @@ class User extends AttachmentContainingDBElement implements UserInterface, HasPe
|
|||
++$this->trustedDeviceCookieVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if U2F is enabled.
|
||||
*/
|
||||
public function isU2FAuthEnabled(): bool
|
||||
{
|
||||
return count($this->u2fKeys) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all U2F Keys that are associated with this user.
|
||||
*
|
||||
* @psalm-return Collection<int, TwoFactorKeyInterface>
|
||||
*/
|
||||
public function getU2FKeys(): Collection
|
||||
{
|
||||
return $this->u2fKeys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a U2F key to this user.
|
||||
*/
|
||||
public function addU2FKey(TwoFactorKeyInterface $key): void
|
||||
{
|
||||
$this->u2fKeys->add($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a U2F key from this user.
|
||||
*/
|
||||
public function removeU2FKey(TwoFactorKeyInterface $key): void
|
||||
{
|
||||
$this->u2fKeys->removeElement($key);
|
||||
}
|
||||
|
||||
public function getPreferredTwoFactorProvider(): ?string
|
||||
{
|
||||
//If U2F is available then prefer it
|
||||
if ($this->isU2FAuthEnabled()) {
|
||||
return 'u2f_two_factor';
|
||||
//if ($this->isU2FAuthEnabled()) {
|
||||
// return 'u2f_two_factor';
|
||||
//}
|
||||
|
||||
if ($this->isWebAuthnAuthenticatorEnabled()) {
|
||||
return 'webauthn_two_factor_provider';
|
||||
}
|
||||
|
||||
//Otherwise use other methods
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isWebAuthnAuthenticatorEnabled(): bool
|
||||
{
|
||||
return count($this->u2fKeys) > 0
|
||||
|| count($this->webauthn_keys) > 0;
|
||||
}
|
||||
|
||||
public function getLegacyU2FKeys(): iterable
|
||||
{
|
||||
return $this->u2fKeys;
|
||||
}
|
||||
|
||||
public function getWebAuthnUser(): PublicKeyCredentialUserEntity
|
||||
{
|
||||
return new PublicKeyCredentialUserEntity(
|
||||
$this->getUsername(),
|
||||
(string) $this->getId(),
|
||||
$this->getFullName(),
|
||||
);
|
||||
}
|
||||
|
||||
public function getWebauthnKeys(): iterable
|
||||
{
|
||||
return $this->webauthn_keys;
|
||||
}
|
||||
|
||||
public function addWebauthnKey(WebauthnKey $webauthnKey): void
|
||||
{
|
||||
$this->webauthn_keys->add($webauthnKey);
|
||||
}
|
||||
}
|
||||
|
|
98
src/Entity/UserSystem/WebauthnKey.php
Normal file
98
src/Entity/UserSystem/WebauthnKey.php
Normal file
|
@ -0,0 +1,98 @@
|
|||
<?php
|
||||
|
||||
namespace App\Entity\UserSystem;
|
||||
|
||||
use App\Entity\Base\TimestampTrait;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Webauthn\PublicKeyCredentialSource as BasePublicKeyCredentialSource;
|
||||
|
||||
/**
|
||||
* @ORM\Table(name="webauthn_keys")
|
||||
* @ORM\Entity()
|
||||
* @ORM\HasLifecycleCallbacks()
|
||||
*/
|
||||
class WebauthnKey extends BasePublicKeyCredentialSource
|
||||
{
|
||||
use TimestampTrait;
|
||||
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue(strategy="AUTO")
|
||||
*/
|
||||
protected int $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
protected string $name;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="App\Entity\UserSystem\User", inversedBy="webauthn_keys")
|
||||
**/
|
||||
protected ?User $user = null;
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $name
|
||||
* @return WebauthnKey
|
||||
*/
|
||||
public function setName(string $name): WebauthnKey
|
||||
{
|
||||
$this->name = $name;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return User|null
|
||||
*/
|
||||
public function getUser(): ?User
|
||||
{
|
||||
return $this->user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param User|null $user
|
||||
* @return WebauthnKey
|
||||
*/
|
||||
public function setUser(?User $user): WebauthnKey
|
||||
{
|
||||
$this->user = $user;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return int
|
||||
*/
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
public static function fromRegistration(BasePublicKeyCredentialSource $registration): self
|
||||
{
|
||||
return new self(
|
||||
$registration->getPublicKeyCredentialId(),
|
||||
$registration->getType(),
|
||||
$registration->getTransports(),
|
||||
$registration->getAttestationType(),
|
||||
$registration->getTrustPath(),
|
||||
$registration->getAaguid(),
|
||||
$registration->getCredentialPublicKey(),
|
||||
$registration->getUserHandle(),
|
||||
$registration->getCounter(),
|
||||
$registration->getOtherUI()
|
||||
);
|
||||
}
|
||||
}
|
|
@ -146,7 +146,7 @@ final class PasswordChangeNeededSubscriber implements EventSubscriberInterface
|
|||
*/
|
||||
public static function TFARedirectNeeded(User $user): bool
|
||||
{
|
||||
$tfa_enabled = $user->isU2FAuthEnabled() || $user->isGoogleAuthenticatorEnabled();
|
||||
$tfa_enabled = $user->isWebAuthnAuthenticatorEnabled() || $user->isGoogleAuthenticatorEnabled();
|
||||
|
||||
return null !== $user->getGroup() && $user->getGroup()->isEnforce2FA() && !$tfa_enabled;
|
||||
}
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published
|
||||
* by the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
|
||||
*
|
||||
* This program is free software; you can redistribute it and/or
|
||||
* modify it under the terms of the GNU General Public License
|
||||
* as published by the Free Software Foundation; either version 2
|
||||
* of the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program; if not, write to the Free Software
|
||||
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
|
||||
*/
|
||||
|
||||
namespace App\EventSubscriber\UserSystem;
|
||||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Events\SecurityEvent;
|
||||
use App\Events\SecurityEvents;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use R\U2FTwoFactorBundle\Event\RegisterEvent;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcher;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Session;
|
||||
use Symfony\Component\HttpFoundation\Session\SessionInterface;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
|
||||
|
||||
/**
|
||||
* This subscriber is used to write U2F keys to DB, after user added them via GUI.
|
||||
*/
|
||||
final class RegisterU2FSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private EntityManagerInterface $em;
|
||||
|
||||
private bool $demo_mode;
|
||||
private FlashBagInterface $flashBag;
|
||||
private UrlGeneratorInterface $router;
|
||||
|
||||
/**
|
||||
* @var EventDispatcher
|
||||
*/
|
||||
private EventDispatcherInterface $eventDispatcher;
|
||||
|
||||
public function __construct(UrlGeneratorInterface $router, EntityManagerInterface $entityManager, SessionInterface $session, EventDispatcherInterface $eventDispatcher, bool $demo_mode)
|
||||
{
|
||||
/** @var Session $session */
|
||||
$this->router = $router;
|
||||
$this->em = $entityManager;
|
||||
$this->demo_mode = $demo_mode;
|
||||
$this->flashBag = $session->getFlashBag();
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
'r_u2f_two_factor.register' => 'onRegister',
|
||||
];
|
||||
}
|
||||
|
||||
public function onRegister(RegisterEvent $event): void
|
||||
{
|
||||
//Skip adding of U2F key on demo mode
|
||||
if (!$this->demo_mode) {
|
||||
$user = $event->getUser();
|
||||
if (!$user instanceof User) {
|
||||
throw new \InvalidArgumentException('Only User objects can be registered for U2F!');
|
||||
}
|
||||
|
||||
$registration = $event->getRegistration();
|
||||
$newKey = new U2FKey();
|
||||
$newKey->fromRegistrationData($registration);
|
||||
$newKey->setUser($user);
|
||||
$newKey->setName($event->getKeyName());
|
||||
|
||||
// persist the new key
|
||||
$this->em->persist($newKey);
|
||||
$this->em->flush();
|
||||
$this->flashBag->add('success', 'tfa_u2f.key_added_successful');
|
||||
|
||||
$security_event = new SecurityEvent($user);
|
||||
$this->eventDispatcher->dispatch($security_event, SecurityEvents::U2F_ADDED);
|
||||
}
|
||||
|
||||
// generate new response, here we redirect the user to the fos user
|
||||
// profile
|
||||
$response = new RedirectResponse($this->router->generate('user_settings'));
|
||||
$event->setResponse($response);
|
||||
}
|
||||
}
|
|
@ -76,6 +76,10 @@ class BackupCodeManager
|
|||
return;
|
||||
}
|
||||
|
||||
if ($user->isWebAuthnAuthenticatorEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user->setBackupCodes([]);
|
||||
}
|
||||
|
||||
|
|
21
symfony.lock
21
symfony.lock
|
@ -176,6 +176,9 @@
|
|||
"imagine/imagine": {
|
||||
"version": "1.2.2"
|
||||
},
|
||||
"jbtronics/2fa-webauthn": {
|
||||
"version": "dev-master"
|
||||
},
|
||||
"laminas/laminas-code": {
|
||||
"version": "3.4.1"
|
||||
},
|
||||
|
@ -252,9 +255,6 @@
|
|||
"openlss/lib-array2xml": {
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"paragonie/random_compat": {
|
||||
"version": "v9.99.99"
|
||||
},
|
||||
"phenx/php-font-lib": {
|
||||
"version": "0.5.1"
|
||||
},
|
||||
|
@ -348,9 +348,6 @@
|
|||
"psr/simple-cache": {
|
||||
"version": "1.0.1"
|
||||
},
|
||||
"r/u2f-two-factor-bundle": {
|
||||
"version": "dev-scheb/2fa-support"
|
||||
},
|
||||
"roave/security-advisories": {
|
||||
"version": "dev-master"
|
||||
},
|
||||
|
@ -397,6 +394,9 @@
|
|||
"shivas/versioning-bundle": {
|
||||
"version": "3.1.3"
|
||||
},
|
||||
"spomky-labs/cbor-bundle": {
|
||||
"version": "v2.0.3"
|
||||
},
|
||||
"symfony/apache-pack": {
|
||||
"version": "1.0",
|
||||
"recipe": {
|
||||
|
@ -817,6 +817,15 @@
|
|||
"vimeo/psalm": {
|
||||
"version": "3.5.1"
|
||||
},
|
||||
"web-auth/webauthn-symfony-bundle": {
|
||||
"version": "3.3",
|
||||
"recipe": {
|
||||
"repo": "github.com/symfony/recipes-contrib",
|
||||
"branch": "main",
|
||||
"version": "3.0",
|
||||
"ref": "9926090a80c2cceeffe96e6c3312b397ea55d4a7"
|
||||
}
|
||||
},
|
||||
"webmozart/assert": {
|
||||
"version": "1.4.0"
|
||||
},
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
<div class="tab-pane fade" id="tfa-u2f" role="tabpanel" aria-labelledby="u2f-tab">
|
||||
<p>{% trans %}tfa_u2f.explanation{% endtrans %}</p>
|
||||
|
||||
{% if user.u2FKeys is not empty %}
|
||||
{% if user.legacyU2FKeys is not empty or user.webauthnKeys is not empty %}
|
||||
<b>{% trans %}tfa_u2f.table_caption{% endtrans %}:</b>
|
||||
<form action="{{ path('u2f_delete') }}" method="post"
|
||||
{{ stimulus_controller('elements/delete_btn') }} {{ stimulus_action('elements/delete_btn', "submit", "submit") }}
|
||||
|
@ -128,14 +128,22 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for key in user.u2FKeys %}
|
||||
{% for key in user.legacyU2FKeys %}
|
||||
<tr>
|
||||
<td>{{ loop.index }}</td>
|
||||
<td>{{ loop.index }} <b>(U2F)</b></td>
|
||||
<td>{{ key.name }}</td>
|
||||
<td>{{ key.addedDate | format_datetime }}</td>
|
||||
<td><button type="submit" class="btn btn-danger btn-sm" name="key_id" value="{{ key.id }}"><i class="fas fa-trash-alt fa-fw"></i> {% trans %}tfa_u2f.key_delete{% endtrans %}</button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% for key in user.webauthnKeys %}
|
||||
<tr>
|
||||
<td>{{ loop.index }} <b>(WebAuthn)</b></td>
|
||||
<td>{{ key.name }}</td>
|
||||
<td>{{ key.addedDate | format_datetime }}</td>
|
||||
<td><button type="submit" class="btn btn-danger btn-sm" name="webauthn_key_id" value="{{ key.id }}"><i class="fas fa-trash-alt fa-fw"></i> {% trans %}tfa_u2f.key_delete{% endtrans %}</button></td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
|
@ -143,7 +151,7 @@
|
|||
<p><b>{% trans %}tfa_u2f.no_keys_registered{% endtrans %}</b></p>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ path('club_base_register_u2f') }}" class="btn btn-success"><i class="fas fa-plus-square fa-fw"></i> {% trans %}tfa_u2f.add_new_key{% endtrans %}</a>
|
||||
<a href="{{ path('webauthn_register') }}" class="btn btn-success"><i class="fas fa-plus-square fa-fw"></i> {% trans %}tfa_u2f.add_new_key{% endtrans %}</a>
|
||||
</div>
|
||||
|
||||
<div class="tab-pane fade" id="tfa-trustedDevices" role="tabpanel" aria-labelledby="trustedDevices-tab-tab">
|
||||
|
|
|
@ -52,7 +52,7 @@
|
|||
|
||||
{% block javascripts %}
|
||||
{{ encore_entry_script_tags('app') }}
|
||||
{{ encore_entry_script_tags('ru2ftwofactor') }}
|
||||
{{ encore_entry_script_tags('webauthn_tfa') }}
|
||||
|
||||
{# load translation files for ckeditor #}
|
||||
{% set two_chars_locale = app.request.locale|default("en")|slice(0,2) %}
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
{% extends "security/2fa_base_form.html.twig" %}
|
||||
|
||||
{% block form_attributes %}id="u2fForm" data-action="auth" data-request='{{ authenticationData|raw }}'{% endblock %}
|
||||
{% block form_attributes %} data-webauthn-tfa-action="authenticate" data-webauthn-tfa-data='{{ webauthn_request_data|raw }}'{% endblock %}
|
||||
|
||||
{% block form %}
|
||||
{% if not app.request.secure %}
|
||||
<p class="text-warning"><b><i class="fas fa-exclamation-triangle fa-fw"></i> {% trans %}tfa_u2f.http_warning{% endtrans %}</b></p>
|
||||
{% endif %}
|
||||
|
||||
<div id="u2fpressbutton" style="display: none;" class="h4 text-center">{{ 'r_u2f_two_factor.pressbutton'|trans }}</div>
|
||||
<div id="u2fError"></div>
|
||||
<p class="widget"><input id="_auth_code" type="hidden" autocomplete="off" name="_auth_code" /></p>
|
||||
<a class="ms-2" href="{{ logoutPath }}">{% trans %}user.logout{% endtrans %}</a>
|
||||
{% endblock %}
|
|
@ -10,13 +10,13 @@
|
|||
<p class="text-warning"><b><i class="fas fa-exclamation-triangle fa-fw"></i> {% trans %}tfa_u2f.http_warning{% endtrans %}</b></p>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="form" action="{{ path('club_base_register_u2f') }}" id="u2fForm" data-action="reg" data-request='{{ registrationRequest|raw }}'>
|
||||
<form method="post" class="form" action="{{ path('webauthn_register') }}" data-webauthn-tfa-action="register" data-webauthn-tfa-data='{{ registrationRequest|raw }}'>
|
||||
<div id="u2fkeyname" class="form-group row">
|
||||
<div class="col-9">
|
||||
<input type="text" class="form-control " name="keyName" id="keyName" placeholder="{{ 'r_u2f_two_factor.name'|trans }}"/>
|
||||
<input type="text" class="form-control " name="keyName" id="keyName" placeholder="Shown key name"/>
|
||||
</div>
|
||||
<div class="col-3">
|
||||
<button type="button" class="btn btn-success" {{ stimulus_controller('pages/u2f_register') }}>{% trans %}tfa_u2f.add_key.add_button{% endtrans %}</button>
|
||||
<button type="submit" class="btn btn-success">{% trans %}tfa_u2f.add_key.add_button{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="u2fpressbutton" style="display: none;" class="text-center h4">{{ 'r_u2f_two_factor.pressbutton'|trans }}</div>
|
|
@ -44,8 +44,12 @@ namespace App\Tests\Entity\UserSystem;
|
|||
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use DateTime;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
class UserTest extends TestCase
|
||||
{
|
||||
|
@ -142,13 +146,26 @@ class UserTest extends TestCase
|
|||
$this->assertGreaterThan($old_value, $user->getTrustedTokenVersion());
|
||||
}
|
||||
|
||||
public function testIsU2fEnabled(): void
|
||||
public function testIsWebauthnEnabled(): void
|
||||
{
|
||||
$user = new User();
|
||||
$user->addU2FKey(new U2FKey());
|
||||
$this->assertTrue($user->isU2FAuthEnabled());
|
||||
$user->addWebauthnKey(new WebauthnKey(
|
||||
"Test",
|
||||
"Test",
|
||||
[],
|
||||
"Test",
|
||||
new EmptyTrustPath(),
|
||||
Uuid::fromDateTime(new \DateTime()),
|
||||
"",
|
||||
"",
|
||||
0
|
||||
));
|
||||
$this->assertTrue($user->isWebAuthnAuthenticatorEnabled());
|
||||
|
||||
$user->getU2FKeys()->clear();
|
||||
$this->assertFalse($user->isU2FAuthEnabled());
|
||||
$result = $user->getWebauthnKeys();
|
||||
if($result instanceof Collection){
|
||||
$result->clear();
|
||||
}
|
||||
$this->assertFalse($user->isWebAuthnAuthenticatorEnabled());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,8 +45,11 @@ namespace App\Tests\EventSubscriber;
|
|||
use App\Entity\UserSystem\Group;
|
||||
use App\Entity\UserSystem\U2FKey;
|
||||
use App\Entity\UserSystem\User;
|
||||
use App\Entity\UserSystem\WebauthnKey;
|
||||
use App\EventSubscriber\UserSystem\PasswordChangeNeededSubscriber;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Ramsey\Uuid\Uuid;
|
||||
use Webauthn\TrustPath\EmptyTrustPath;
|
||||
|
||||
class PasswordChangeNeededSubscriberTest extends TestCase
|
||||
{
|
||||
|
@ -73,7 +76,17 @@ class PasswordChangeNeededSubscriberTest extends TestCase
|
|||
|
||||
//User must not be redirect if 2FA is setup
|
||||
$user->setGoogleAuthenticatorSecret(null);
|
||||
$user->addU2FKey(new U2FKey());
|
||||
$user->addWebauthnKey(new WebauthnKey(
|
||||
"Test",
|
||||
"Test",
|
||||
[],
|
||||
"Test",
|
||||
new EmptyTrustPath(),
|
||||
Uuid::fromDateTime(new \DateTime()),
|
||||
"",
|
||||
"",
|
||||
0
|
||||
));
|
||||
$this->assertFalse(PasswordChangeNeededSubscriber::TFARedirectNeeded($user));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8691,13 +8691,9 @@ Element 3</target>
|
|||
<target>Google</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tST2X1R" name="tfa.provider.u2f_two_factor">
|
||||
<notes>
|
||||
<note priority="1">obsolete</note>
|
||||
<note category="state" priority="1">obsolete</note>
|
||||
</notes>
|
||||
<unit id="tST2X1S" name="tfa.provider.webauthn_two_factor_provider">
|
||||
<segment state="translated">
|
||||
<source>tfa.provider.u2f_two_factor</source>
|
||||
<source>tfa.provider.webauthn_two_factor_provider</source>
|
||||
<target>Sicherheitsschlüssel</target>
|
||||
</segment>
|
||||
</unit>
|
||||
|
|
|
@ -8692,14 +8692,10 @@ Element 3</target>
|
|||
<target>Google</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="tST2X1R" name="tfa.provider.u2f_two_factor">
|
||||
<notes>
|
||||
<note priority="1">obsolete</note>
|
||||
<note category="state" priority="1">obsolete</note>
|
||||
</notes>
|
||||
<unit id="tST2X1S" name="tfa.provider.webauthn_two_factor_provider">
|
||||
<segment state="translated">
|
||||
<source>tfa.provider.u2f_two_factor</source>
|
||||
<target>Security key (U2F)</target>
|
||||
<source>tfa.provider.webauthn_two_factor_provider</source>
|
||||
<target>Security key</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="XTe6pUU" name="tfa.provider.google">
|
||||
|
|
|
@ -59,7 +59,7 @@ Encore
|
|||
* and one CSS file (e.g. app.css) if you JavaScript imports CSS.
|
||||
*/
|
||||
.addEntry('app', './assets/js/app.js')
|
||||
.addEntry('ru2ftwofactor', './assets/js/u2f_auth.js')
|
||||
.addEntry('webauthn_tfa', './assets/js/webauthn_tfa.js')
|
||||
|
||||
// enables the Symfony UX Stimulus bridge (used in assets/bootstrap.js)
|
||||
.enableStimulusBridge('./assets/controllers.json')
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue