Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions ASVS/V06-Authentication.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This section contains requirements detailing the authentication documentation th
|---|-------|-------------|--------|---------------|------------|
| 6.1.1 | 1 | Verify that application documentation defines how controls such as rate limiting, anti‑automation, and adaptive response, are used to defend against attacks such as credential stuffing and password brute force. The documentation must make clear how these controls are configured and prevent malicious account lockout. | Partial | Rate limiting configured via setLoginAttemptProtection(). Documentation of controls not yet formalized in ASVS format. | Per-project: document `setLoginAttemptProtection($maxAttempts, $timeout)` values and rationale in project security docs. |
| 6.1.2 | 2 | Verify that a list of context‑specific words is documented in order to prevent their use in passwords. The list could include permutations of organization names, product names, system identifiers, project codenames, department or role names, and similar. | Partial | Context-specific word list not yet documented or implemented. | Per-project: create context-specific word list (org name, product name, domain) for password deny list. |
| 6.1.3 | 2 | Verify that, if the application includes multiple authentication pathways, these are all documented together with the security controls and authentication strength which must be consistently enforced across them. | Compliant | Single authentication pathway (email/username + password). No multiple pathways. | — |
| 6.1.3 | 2 | Verify that, if the application includes multiple authentication pathways, these are all documented together with the security controls and authentication strength which must be consistently enforced across them. | Compliant | Authentication pathways documented in README: email + password, passkeys (WebAuthn, user verification required), optional Keycloak SSO. Same ACL check (customer/backoffice resource) enforced on all pathways; SSO-bound identities cannot use password or passkey login. | — |

## V6.2 Password Security

Expand Down Expand Up @@ -39,8 +39,8 @@ This section contains general requirements for the security of authentication me
|---|-------|-------------|--------|---------------|------------|
| 6.3.1 | 1 | Verify that controls to prevent attacks such as credential stuffing and password brute force are implemented according to the application's security documentation. | Compliant | IP-based rate limiting via DoctrineAuthenticator. Configurable maxLoginAttempts and loginAttemptTimeout. | — |
| 6.3.2 | 1 | Verify that default user accounts (e.g., "root", "admin", or "sa") are not present in the application or are disabled. | Compliant | No default user accounts. All accounts created through application flows. | — |
| 6.3.3 | 2 | Verify that either a multi‑factor authentication mechanism or a combination of single‑factor authentication mechanisms, must be used in order to access the application. For L3, one of the factors must be a hardware‑based authentication mechanism which provides compromise and impersonation resistance against phishing attacks while verifying the intent to authenticate by requiring a user‑initiated action (such as a button press on a FIDO hardware key or a mobile phone). Relaxing any of the considerations in this requirement requires a fully documented rationale and a comprehensive set of mitigating controls. | Partial | MFA not yet implemented. Single-factor authentication (password) only. MFA is required for L2 compliance. | Pending fancyadmin: implement TOTP MFA. `IdentityTrait` already uses `DoctrineAuthenticator\OTP\IdentityTrait` as foundation. |
| 6.3.4 | 2 | Verify that, if the application includes multiple authentication pathways, there are no undocumented pathways and that security controls and authentication strength are enforced consistently. | Compliant | Single authentication pathway (email + password). No undocumented pathways. | — |
| 6.3.3 | 2 | Verify that either a multi‑factor authentication mechanism or a combination of single‑factor authentication mechanisms, must be used in order to access the application. For L3, one of the factors must be a hardware‑based authentication mechanism which provides compromise and impersonation resistance against phishing attacks while verifying the intent to authenticate by requiring a user‑initiated action (such as a button press on a FIDO hardware key or a mobile phone). Relaxing any of the considerations in this requirement requires a fully documented rationale and a comprehensive set of mitigating controls. | Partial | Passkeys (WebAuthn) implemented — phishing-resistant, hardware-backed, multi-factor in itself (possession + `userVerification: required`, i.e. biometrics/PIN), with user-initiated action. Password remains an alternative pathway, so MFA is not yet *enforced* for every login. | Per-project: for strict L2/L3 compliance, enforce passkey-only or passkey+password policy (passkey infrastructure is now available in fancyadmin). |
| 6.3.4 | 2 | Verify that, if the application includes multiple authentication pathways, there are no undocumented pathways and that security controls and authentication strength are enforced consistently. | Compliant | All pathways documented (password, passkey, optional SSO). Consistent controls: same ACL login check, inactive identities rejected everywhere, SSO identities rejected on password and passkey pathways, passkey requires user verification. | — |
| 6.3.5 | 3 | Verify that users are notified of suspicious authentication attempts (successful or unsuccessful). This may include authentication attempts from an unusual location or client, partially successful authentication (only one of multiple factors), an authentication attempt after a long period of inactivity or a successful authentication after several unsuccessful attempts. | | | |
| 6.3.6 | 3 | Verify that email is not used as either a single‑factor or multi‑factor authentication mechanism. | | | |
| 6.3.7 | 3 | Verify that users are notified after updates to authentication details, such as credential resets or modification of the username or email address. | | | |
Expand Down Expand Up @@ -70,7 +70,7 @@ This section provides general guidance that will be relevant to various differen
| 6.5.3 | 2 | Verify that lookup secrets, out‑of‑band authentication code, and time‑based one‑time password seeds, are generated using a Cryptographically Secure Pseudorandom Number Generator (CSPRNG) to avoid predictable values. | Out of scope | MFA not yet implemented. These requirements apply when MFA is added. | — |
| 6.5.4 | 2 | Verify that lookup secrets and out‑of‑band authentication codes have a minimum of 20 bits of entropy (typically 4 random alphanumeric characters or 6 random digits is sufficient). | Out of scope | MFA not yet implemented. These requirements apply when MFA is added. | — |
| 6.5.5 | 2 | Verify that out‑of‑band authentication requests, codes, or tokens, as well as time‑based one‑time passwords (TOTPs) have a defined lifetime. Out of band requests must have a maximum lifetime of 10 minutes and for TOTP a maximum lifetime of 30 seconds. | Out of scope | MFA not yet implemented. These requirements apply when MFA is added. | — |
| 6.5.6 | 3 | Verify that any authentication factor (including physical devices) can be revoked in case of theft or other loss. | | | |
| 6.5.6 | 3 | Verify that any authentication factor (including physical devices) can be revoked in case of theft or other loss. | Compliant | Passkeys can be deleted by the user on the Account page (only own keys); password can be reset. Admin can deactivate the identity, which blocks all pathways including passkey login. | — |
| 6.5.7 | 3 | Verify that biometric authentication mechanisms are only used as secondary factors together with either something you have or something you know. | | | |
| 6.5.8 | 3 | Verify that time‑based one‑time passwords (TOTPs) are checked based on a time source from a trusted service and not from an untrusted or client provided time. | | | |

Expand All @@ -91,8 +91,8 @@ Cryptographic authentication mechanisms include smart cards or FIDO keys, where

| # | Level | Requirement | Status | How We Comply | What to Do |
|---|-------|-------------|--------|---------------|------------|
| 6.7.1 | 3 | Verify that the certificates used to verify cryptographic authentication assertions are stored in a way protects them from modification. | | | |
| 6.7.2 | 3 | Verify that the challenge nonce is at least 64 bits in length, and statistically unique or unique over the lifetime of the cryptographic device. | | | |
| 6.7.1 | 3 | Verify that the certificates used to verify cryptographic authentication assertions are stored in a way protects them from modification. | Compliant | Passkey public keys (PEM) are stored server-side in the `passkey` table and are never modifiable by the user — only the key name can be renamed; verification data is written exclusively by `PasskeyService::processRegistration()`. | — |
| 6.7.2 | 3 | Verify that the challenge nonce is at least 64 bits in length, and statistically unique or unique over the lifetime of the cryptographic device. | Compliant | WebAuthn challenge is 32 bytes (256 bits) from a CSPRNG (`ByteBuffer::randomBuffer(32)`), single-use (removed from session on first read) and expires after 5 minutes. | — |

## V6.8 Authentication with an Identity Provider

Expand Down
222 changes: 222 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1237,6 +1237,227 @@ Pro použití vlastní třídy je potřeba rozšířit `KeycloakManager::createI

---

## 19. Passkeys (WebAuthn)

Fancyadmin podporuje přihlašování přes passkeys (WebAuthn) postavené na knihovně
[lbuchs/webauthn](https://github.com/lbuchs/WebAuthn). Passkeys jsou **vždy zapnuté** —
žádný config flag; passkey je vždy jen alternativa k heslu (žádné passkey-only účty).
Identity navázané na Keycloak SSO se přes passkey přihlásit ani registrovat klíč nemohou
(autorita pro SSO účty je Keycloak).

Co uživatel dostane:

- **Login stránka** — tlačítko „Přihlásit se přihlašovacím klíčem" (usernameless login,
prohlížeč nabídne uložené discoverable credentials). Tlačítko je jediná cesta —
passkey se **nenabízí automaticky** v autofillu email pole (conditional mediation
není zapnutá)
- **Můj účet** — karta „Přihlašovací klíče": přidání klíče (side panel s povinným názvem),
smazání, badge pro synchronizované klíče (zálohované u správce passkeys)

### 19.1 Požadavky

- **HTTPS** — WebAuthn funguje jen v secure kontextu (výjimka: `localhost`)
- **rpId = doména admin hostu** — klíče jsou svázané s doménou; změna domény znamená
ztrátu registrovaných klíčů. Default se odvozuje z `adminHostPath`.

### 19.2 NEON konfigurace (volitelné)

```neon
fancyadmin:
# ... ostatní konfigurace ...
# Relying Party ID — doména; když není nastaveno, odvodí se host z adminHostPath
passkeyRpId: admin.muj-projekt.cz
# Relying Party name — zobrazuje se v dialogu autentikátoru; default = projectName
passkeyRpName: Můj projekt
```

### 19.3 Entita Passkey

```php
// app/Model/Entities/Passkey.php
<?php

declare(strict_types=1);

namespace App\Model\Entities;

use ADT\FancyAdmin\Model\Entities\PasskeyTrait;
use App\Model\Entities\Abstract\BaseEntity;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
class Passkey extends BaseEntity implements \ADT\FancyAdmin\Model\Entities\Passkey
{
use PasskeyTrait;
}
```

**PasskeyTrait poskytuje:**

| Sloupec | Typ | Popis |
|---|---|---|
| `identity` | Identity (FK, ON DELETE CASCADE) | Vlastník klíče |
| `name` | VARCHAR(64) | Uživatelský název klíče |
| `credentialId` | VARBINARY(255), unique | Raw binary credential ID |
| `publicKey` | TEXT | Veřejný klíč (PEM) |
| `signCount` | INT UNSIGNED | Signature counter (detekce klonu) |
| `aaguid` | BINARY(16), nullable | AAGUID autentikátoru |
| `transports` | JSON, nullable | Transports z prohlížeče |
| `backupEligible` / `backupState` | BOOL, nullable | Backup flags (synchronizovaný klíč) |
| `createdAt` | DATETIME | Vytvořeno |
| `lastUsedAt` | DATETIME, nullable | Poslední přihlášení klíčem |

`IdentityTrait` navíc přidává do tabulky `identity` nullable sloupec `passkey_user_handle`
(BINARY(32)) — náhodný opaque WebAuthn user handle, generovaný při registraci prvního klíče
(autentikátoru se nikdy neposílá interní ID identity).

### 19.4 Query + factory

```php
// app/Model/Queries/PasskeyQuery.php
<?php

declare(strict_types=1);

namespace App\Model\Queries;

use ADT\FancyAdmin\Model\Entities\Account;
use ADT\FancyAdmin\Model\Queries\PasskeyQueryTrait;
use App\Model\Entities\Passkey;
use Doctrine\ORM\QueryBuilder;

/**
* @extends Base\BaseQuery<Passkey>
*/
class PasskeyQuery extends Base\BaseQuery implements \ADT\FancyAdmin\Model\Queries\PasskeyQuery
{
use PasskeyQueryTrait;

protected function applySecurityFilter(): void {}
protected function applyAccountFilter(QueryBuilder $qb, Account $account): void {}
}
```

```php
// app/Model/Queries/Factories/PasskeyQueryFactory.php
<?php

namespace App\Model\Queries\Factories;

use App\Model\Queries\PasskeyQuery;

interface PasskeyQueryFactory extends \ADT\FancyAdmin\Model\Queries\Factories\PasskeyQueryFactory
{
public function create(): PasskeyQuery;
}
```

### 19.5 Form + grid (Account stránka)

```php
// app/UI/Portal/Components/Forms/Passkey/PasskeyForm.php
<?php

declare(strict_types=1);

namespace App\UI\Portal\Components\Forms\Passkey;

use ADT\FancyAdmin\UI\Components\Forms\Passkey\PasskeyFormTrait;
use App\UI\Portal\Components\Forms\Base\BaseForm;

class PasskeyForm extends BaseForm implements \ADT\FancyAdmin\UI\Components\Forms\Passkey\PasskeyForm
{
use PasskeyFormTrait;
}
```

```php
// app/UI/Portal/Components/Forms/Passkey/PasskeyFormFactory.php
<?php

declare(strict_types=1);

namespace App\UI\Portal\Components\Forms\Passkey;

interface PasskeyFormFactory extends \ADT\FancyAdmin\UI\Components\Forms\Passkey\PasskeyFormFactory
{
public function create(): PasskeyForm;
}
```

```php
// app/UI/Portal/Components/Grids/Passkey/PasskeyGrid.php
<?php

declare(strict_types=1);

namespace App\UI\Portal\Components\Grids\Passkey;

use ADT\Datagrid\Component\DataGrid;
use ADT\FancyAdmin\UI\Components\Grids\Passkey\PasskeyGridTrait;
use App\UI\Portal\Components\Grids\Base\BaseGrid;

class PasskeyGrid extends BaseGrid implements \ADT\FancyAdmin\UI\Components\Grids\Passkey\PasskeyGrid
{
use PasskeyGridTrait {
initGrid as initGridTrait;
}

public function initGrid(DataGrid $grid): void
{
parent::initGrid($grid);
$this->initGridTrait($grid);
}
}
```

```php
// app/UI/Portal/Components/Grids/Passkey/PasskeyGridFactory.php
<?php

declare(strict_types=1);

namespace App\UI\Portal\Components\Grids\Passkey;

interface PasskeyGridFactory extends \ADT\FancyAdmin\UI\Components\Grids\Passkey\PasskeyGridFactory
{
public function create(): PasskeyGrid;
}
```

Factory interfaces se registrují automaticky přes stávající `search` sekce v neonu
(`*Factory.php` v `Model/Queries` a `UI/Portal/Components`).

### 19.6 Migrace

Knihovna **žádnou migraci nedodává** — schéma vlastní projekt:

```bash
php bin/console migrations:diff
php bin/console migrations:migrate
```

Vytvoří tabulku `passkey` a přidá sloupec `identity.passkey_user_handle`.

### 19.7 Jak to funguje (bezpečnostní poznámky)

- Attestation format `none` (standard pro passkeys), `residentKey: required`
(discoverable credentials), `userVerification: required`
- Login je usernameless — prázdné `allowCredentials`, klíč se hledá podle credential ID
z assertion (credential-first lookup); `userHandle` se ověřuje proti
`identity.passkey_user_handle` přes `hash_equals()`
- Challenge se drží v Nette session, one-shot (po přečtení se maže), expirace 5 minut,
oddělené klíče pro registraci a login
- Všechny binárky v JSON jsou base64url (`PublicKeyCredential.toJSON()` formát)
- Signature counter se ověřuje (`lbuchs/webauthn` vyhodí chybu při poklesu — možný klon klíče)
- Neaktivní identita a SSO identita se klíčem nepřihlásí; po loginu platí stejný ACL check
jako u hesla (customer/backoffice resource)
- Ceremony se spouští jen kliknutím na tlačítko — na server nejde žádný request, dokud
uživatel neklikne, takže anonymní návštěvník login stránky nedostane session cookie
(challenge se do session zapisuje až v okamžiku ceremony)

---

## Shrnutí

| Krok | Co | Proč |
Expand All @@ -1254,3 +1475,4 @@ Pro použití vlastní třídy je potřeba rozšířit `KeycloakManager::createI
| Translator | Rozšiřuje Contributte\Translation\Translator | Překlady |
| RouterFactory | Integruje FancyAdminRouter | Sign routes, portal routes |
| Portal presentery | BasePresenter + AuthPresenter s fancyadmin traits | Admin layout, auth check, side panel |
| Passkey glue třídy | Entita Passkey, PasskeyQuery + factory, PasskeyForm + factory, PasskeyGrid + factory | Přihlašování přes passkeys (WebAuthn) — viz sekce 19 |
2 changes: 2 additions & 0 deletions assets/js/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,8 @@ import AdtJsComponents from 'adt-js-components';
// AdtJsComponents.init('components-panels-base-baseChartPanel', 'UI/Portal/Components/Panels/Base/BaseChartPanelControl');
AdtJsComponents.init('select-account-form', '~UI/Components/Forms/SelectAccount');
AdtJsComponents.init('portal-components-grids-traits-signInAsIdentity', '~UI/Components/Grids/Traits/SignInAsIdentity');
AdtJsComponents.init('fancyadmin-passkey-login', '~UI/Components/Forms/SignIn');
AdtJsComponents.init('fancyadmin-passkey-form', '~UI/Components/Forms/Passkey');
// AdtJsComponents.init('portal-components-forms-dashboardFilter', 'UI/Portal/Components/Forms/DashboardFilter');
// AdtJsComponents.init('portal-components-forms-changeLicenceForm', 'UI/Portal/Components/Forms/ChangeLicence');
// AdtJsComponents.init('portal-components-forms-warehouseOperationForm', 'UI/Portal/Components/Forms/WarehouseOperation');
Expand Down
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"adt/datagrid-components": "^1.0",
"nettrine/orm": "^0.10",
"adt/route-port": "^3.0",
"adt/doctrine-loggable": "^3.0"
"adt/doctrine-loggable": "^3.0",
"lbuchs/webauthn": "^2.2"
},
"suggest": {
"guzzlehttp/guzzle": "Required for Keycloak SSO integration",
Expand Down
Loading